diff --git a/build/local_examples.py b/build/local_examples.py index 923bf8579d..9a36f93312 100644 --- a/build/local_examples.py +++ b/build/local_examples.py @@ -104,10 +104,12 @@ def get_client_name_from_language_and_path(language: str, path: str, For Python (.py) files, override based on path substrings: - If 'redisvl' in path -> RedisVL + - If 'agent-memory' in path -> Agent Memory (Python) - If 'iris' in path -> Python (Iris) - Otherwise -> Python For TypeScript (.ts, .mts) and shell (.sh) files, override based on path substrings: + - If 'agent-memory' in path -> Agent Memory (TypeScript) - If 'iris' in path -> TypeScript (Iris) or cURL (Iris) Substring checks are case-sensitive and can appear anywhere in the path. @@ -130,9 +132,13 @@ def get_client_name_from_language_and_path(language: str, path: str, if language == 'python': if 'redisvl' in path: return 'RedisVL' + if 'agent-memory' in path: + return 'Agent Memory (Python)' if 'iris' in path: return 'Python (Iris)' if language == 'typescript': + if 'agent-memory' in path: + return 'Agent Memory (TypeScript)' if 'iris' in path: return 'TypeScript (Iris)' if language == 'bash': diff --git a/config.toml b/config.toml index 057b3e143b..890a712197 100644 --- a/config.toml +++ b/config.toml @@ -87,7 +87,7 @@ gitHubRepo = "https://github.com/redis/docs" aiSchemaVersion = 2 # Display and sort order for client examples -clientsExamples = ["Python", "Node.js", "ioredis", "Java-Sync", "Lettuce-Sync", "Java-Async", "Java-Reactive", "Go", "C", "C#-Sync (NRedisStack)", "C#-Async (NRedisStack)", "C#-Sync (SE.Redis)", "C#-Async (SE.Redis)", "RedisVL", "PHP", "Ruby", "Rust-Sync", "Rust-Async", "Python (Iris)", "TypeScript (Iris)", "cURL (Iris)", "CLI (Iris)"] +clientsExamples = ["Python", "Node.js", "ioredis", "Java-Sync", "Lettuce-Sync", "Java-Async", "Java-Reactive", "Go", "C", "C#-Sync (NRedisStack)", "C#-Async (NRedisStack)", "C#-Sync (SE.Redis)", "C#-Async (SE.Redis)", "RedisVL", "PHP", "Ruby", "Rust-Sync", "Rust-Async", "Python (Iris)", "TypeScript (Iris)", "cURL (Iris)", "CLI (Iris)", "Agent Memory (Python)", "Agent Memory (TypeScript)"] searchService = "/convai/api/search-service" ratingsService = "/docusight/api/rate/docs" @@ -125,6 +125,9 @@ rdi_current_version = "2.0.0" "cURL (Iris)"={quickstartSlug="", langId="bash", clientId="iris-curl", clientName="cURL (Iris)", mappingClientId=""} "CLI (Iris)"={quickstartSlug="", langId="bash", clientId="iris-cli", clientName="CLI (Iris)", mappingClientId=""} +"Agent Memory (Python)"={quickstartSlug="", langId="python", clientId="agent-memory-python", clientName="Agent Memory (Python)", mappingClientId=""} +"Agent Memory (TypeScript)"={quickstartSlug="", langId="typescript", clientId="agent-memory-typescript", clientName="Agent Memory (TypeScript)", mappingClientId=""} + # Mount directories for duplicate content [module] [[module.mounts]] diff --git a/content/develop/ai/context-engine/agent-memory/_index.md b/content/develop/ai/context-engine/agent-memory/_index.md index 92a828ae16..69edb9f244 100644 --- a/content/develop/ai/context-engine/agent-memory/_index.md +++ b/content/develop/ai/context-engine/agent-memory/_index.md @@ -9,8 +9,6 @@ description: Store agent memory for AI applications in Redis. linkTitle: Agent Memory hideListLinks: true weight: 10 -bannerText: Redis Agent Memory is currently available in preview. Features and behavior are subject to change. -bannerChildren: true aliases: - /develop/ai/agent-memory/ --- @@ -23,11 +21,13 @@ When enabled, automatic summarization compacts session memory by summarizing old Access Redis Agent Memory through the Python and TypeScript SDKs or its REST API. It works with any agent framework or LLM provider. -
- {{< tile-card icon="images/icon_logo/icon-data-structures-64-midnight.png" title="Overview" description="How session and long-term memory work, and what runs in the background" url="/develop/ai/context-engine/agent-memory/overview" >}} - {{< tile-card icon="images/dev/icons/icon-redis-code-64-duotone.png" title="Python quickstart" description="Get started with Redis Agent Memory in Python" url="/develop/ai/context-engine/agent-memory/python-sdk-quickstart" >}} - {{< tile-card icon="images/dev/icons/icon-redis-code-64-duotone.png" title="TypeScript quickstart" description="Get started with Redis Agent Memory in TypeScript" url="/develop/ai/context-engine/agent-memory/typescript-sdk-quickstart" >}} - {{< tile-card icon="images/dev/icons/icon-redis-code-64-duotone.png" title="REST quickstart" description="Get started with Redis Agent Memory using curl" url="/develop/ai/context-engine/agent-memory/rest-api-quickstart" >}} +
+ {{< tile-card icon="images/icon_logo/icon-data-structures-64-midnight.png" title="Overview" description="What's the same and what's different if you already know Redis" url="/develop/ai/context-engine/agent-memory/overview" >}} + {{< tile-card icon="images/icon_logo/icon-developers-64-midnight.png" title="Quickstart" description="Make your first requests" url="/develop/ai/context-engine/agent-memory/quickstart" >}} + {{< tile-card icon="images/dev/icons/icon-redis-code-64-duotone.png" title="Developer guide" description="Add memory to your application" url="/develop/ai/context-engine/agent-memory/developer-guide" >}} + {{< tile-card icon="images/dev/icons/icon-redis-agent-memory-64-duotone.png" title="Sessions" description="Store conversation events" url="/develop/ai/context-engine/agent-memory/sessions" >}} + {{< tile-card icon="images/dev/icons/icon-redis-agent-memory-64-duotone.png" title="Long-term memory" description="Recall information across conversations" url="/develop/ai/context-engine/agent-memory/long-term-memory" >}} + {{< tile-card icon="images/icon_logo/icon-data-structures-64-midnight.png" title="Namespaces" description="Organize your memories" url="/develop/ai/context-engine/agent-memory/namespaces" >}}
## Why use Redis Agent Memory? @@ -36,7 +36,7 @@ Access Redis Agent Memory through the Python and TypeScript SDKs or its REST API * **Automatic session summarization:** Automatically summarize older conversation events while retaining recent messages in full. * **Automatic long-term memory:** Automatically extract durable information from session events in the background. You can also create long-term memories directly from external data. * **Sensitive-data exclusions:** Guide automatic extraction away from information that should not be kept in long-term memory. -* **Relevant retrieval:** Search long-term memory using semantic, keyword, or hybrid search. +* **Relevant retrieval:** Search long-term memory semantically and narrow results with filters. * **Multi-session recall:** Retrieve relevant memories across conversations and filter results by owner, session, namespace, topic, or memory type. * **Custom memory types:** Define memory types for your business domain, with structured fields and instructions that control what Redis Agent Memory extracts. @@ -55,7 +55,7 @@ Redis Agent Memory provides two memory tiers:

Long-term memory

-

Stores durable information that can be retrieved across sessions using semantic, keyword, or hybrid search.

+

Stores durable information that can be retrieved across sessions using semantic search and filters.

-To watch both tiers change during a conversation, try the [interactive demo](/content/develop/ai/context-engine/agent-memory/interactive-demo.md). - ### Example: Travel planning agent Consider a travel agent helping a user plan a trip: @@ -94,13 +92,13 @@ Agent: Good to know! I'll help you find some vegetarian-friendly restaurants in

Custom memory types

-

Stores domain-specific information in a custom trip_preference type with fields such as destination, travel_period, and dietary_requirement.

+

Stores domain-specific information in a custom trip_preference type with fields such as destinations, travel_period, and dietary_requirements.

## Get started with Redis Agent Memory {#get-started} -Get started with Redis Agent Memory on Redis Cloud or join the private preview for Redis Software. +Get started with Redis Agent Memory on Redis Cloud or deploy it with Redis Software.
@@ -109,34 +107,28 @@ Get started with Redis Agent Memory on Redis Cloud or join the private preview f

}}">Open the Redis Cloud setup guide

-

Redis Software private preview

+

Redis Software

Deploy Redis Agent Memory on Kubernetes with Redis Software.

}}">Open the self-managed deployment guide

-### Choose a quickstart +### Follow the quickstart -After your Redis Agent Memory service is ready, choose a client. Each quickstart follows the same travel planning scenario through session memory, automatic extraction, summarization, custom memory types, and sensitive-data exclusions. +After your service is ready, follow the [quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}). Choose Python, TypeScript, or curl to explore the same travel planning scenario. -
-
-

Python SDK

-

Explore the Redis Agent Memory workflow with the Python SDK.

-

}}">Open the Python quickstart

-
-
-

TypeScript SDK

-

Explore the Redis Agent Memory workflow with the TypeScript SDK.

-

}}">Open the TypeScript quickstart

-
-
-

REST API

-

Explore the Redis Agent Memory workflow with curl.

-

}}">Open the REST API quickstart

-
-
+## Explore the documentation + +* [Overview]({{< relref "/develop/ai/context-engine/agent-memory/overview" >}}): Understand session memory, long-term memory, and namespaces. +* [Quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}): Store a conversation and recall an extracted memory. +* [Developer guide]({{< relref "/develop/ai/context-engine/agent-memory/developer-guide" >}}): Put the pieces together in an application. +* [Sessions]({{< relref "/develop/ai/context-engine/agent-memory/sessions" >}}): Store conversation events and configure retention and summarization. +* [Long-term memory]({{< relref "/develop/ai/context-engine/agent-memory/long-term-memory" >}}): Create and retrieve memories across conversations, including custom memory types. +* [MemoryETL](/content/develop/ai/context-engine/agent-memory/memory-etl.md): Extract memories from documents and inspect their source evidence. +* [Summary Views](/content/develop/ai/context-engine/agent-memory/summary-views.md): Materialize text or structured summaries of long-term memories. +* [Ingestion webhooks](/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md): Review input and generated changes before they are saved. +* [Namespaces]({{< relref "/develop/ai/context-engine/agent-memory/namespaces" >}}): Organize memories and manage namespace hierarchies. -For shared integration concepts, identifiers, and authentication, see the [Redis Agent Memory developer guide](/content/develop/ai/context-engine/agent-memory/developer-guide.md). +* [API reference]({{< relref "/develop/ai/context-engine/agent-memory/api-reference" >}}): Look up request and response schemas. -To generate a complete conversational agent that uses Agent Memory, open the [AI agent builder](/content/develop/ai/agent-builder/_index.md) and select **Redis Iris Conversational Assistant**. +For shared placement rules, see [Namespace routing](/content/develop/ai/context-engine/agent-memory/namespaces/namespace-routing.md). To build a conversation list, see [List and sort sessions](/content/develop/ai/context-engine/agent-memory/sessions/list-sessions.md). diff --git a/content/develop/ai/context-engine/agent-memory/api-reference/openapi-agent-memory.json b/content/develop/ai/context-engine/agent-memory/api-reference/openapi-agent-memory.json index bbab8e30d1..8ae68ff124 100644 --- a/content/develop/ai/context-engine/agent-memory/api-reference/openapi-agent-memory.json +++ b/content/develop/ai/context-engine/agent-memory/api-reference/openapi-agent-memory.json @@ -6,53 +6,50 @@ "description": "API for storing, retrieving, and searching Redis Agent Memory session memory and long-term memory.\n\nAuthentication depends on your deployment. Use the credential issued for your Redis Agent Memory service or store. For self-managed deployments, see the self-managed Redis Agent Memory guide for authentication options.\n" }, "paths": { - "/health": { - "get": { - "description": "Return information about the operational status of the service.", - "operationId": "Health", - "responses": { - "200": { - "description": "Health 200 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthResponseContent" - } + "/v1/stores/{storeId}/document-ingestions": { + "post": { + "description": "Submit one revisioned document for extraction.", + "operationId": "CreateDocumentIngestion", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDocumentIngestionRequestContent" } } - } + }, + "required": true }, - "tags": [ - "Service Health" - ] - } - }, - "/v1/stores/{storeId}/health": { - "get": { - "description": "Returns read-only health for applicable store-scoped RAM features.", - "operationId": "StoreHealth", "parameters": [ { "name": "storeId", "in": "path", - "description": "The store instance ID.", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The store instance ID." + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." }, "required": true } ], "responses": { - "200": { - "description": "StoreHealth 200 response", + "202": { + "description": "CreateDocumentIngestion 202 response", + "headers": { + "Location": { + "schema": { + "type": "string" + }, + "required": true + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StoreHealthResponseContent" + "$ref": "#/components/schemas/CreateDocumentIngestionResponseContent" } } } @@ -107,6 +104,16 @@ } } }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, "413": { "description": "PayloadTooLargeError 413 response", "content": { @@ -117,12 +124,32 @@ } } }, + "422": { + "description": "IngestionHookStoppedError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestionHookStoppedErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, "424": { - "description": "FailedDependencyError 424 response", + "description": "MemoryFailedDependencyError 424 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + "$ref": "#/components/schemas/MemoryFailedDependencyErrorResponseContent" } } } @@ -146,49 +173,58 @@ } } } + }, + "503": { + "description": "IngestionHookRetryError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestionHookRetryErrorResponseContent" + } + } + } } - }, - "tags": [ - "Store Health" - ] + } } }, - "/v1/stores/{storeId}/long-term-memory": { - "delete": { - "description": "Deletes long-term memories in bulk by their IDs.", - "operationId": "BulkDeleteLongTermMemories", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkDeleteLongTermMemoriesRequestContent" - } - } - }, - "required": true - }, + "/v1/stores/{storeId}/document-ingestions/{ingestionId}": { + "get": { + "operationId": "GetDocumentIngestion", "parameters": [ { "name": "storeId", "in": "path", - "description": "The store instance ID.", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The store instance ID." + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "ingestionId", + "in": "path", + "description": "Server-generated document ingestion identifier.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated document ingestion identifier." }, "required": true } ], "responses": { "200": { - "description": "BulkDeleteLongTermMemories 200 response", + "description": "GetDocumentIngestion 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkDeleteLongTermMemoriesResponseContent" + "$ref": "#/components/schemas/GetDocumentIngestionResponseContent" } } } @@ -243,12 +279,12 @@ } } }, - "413": { - "description": "PayloadTooLargeError 413 response", + "423": { + "description": "ResourceSuspendedError 423 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" } } } @@ -263,16 +299,6 @@ } } }, - "429": { - "description": "TooManyRequestsError 429 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" - } - } - } - }, "500": { "description": "UnexpectedError 500 response", "content": { @@ -283,46 +309,63 @@ } } } - }, - "tags": [ - "Long-Term Memory" - ] - }, - "post": { - "description": "Creates long-term memories in bulk.", - "operationId": "BulkCreateLongTermMemories", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkCreateLongTermMemoriesRequestContent" - } - } - }, - "required": true - }, + } + } + }, + "/v1/stores/{storeId}/document-ingestions/{ingestionId}/outputs": { + "get": { + "operationId": "ListDocumentIngestionOutputs", "parameters": [ { "name": "storeId", "in": "path", - "description": "The store instance ID.", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The store instance ID." + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "ingestionId", + "in": "path", + "description": "Server-generated document ingestion identifier.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated document ingestion identifier." }, "required": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } } ], "responses": { - "201": { - "description": "BulkCreateLongTermMemories 201 response", + "200": { + "description": "ListDocumentIngestionOutputs 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkCreateLongTermMemoriesResponseContent" + "$ref": "#/components/schemas/ListDocumentIngestionOutputsResponseContent" } } } @@ -377,12 +420,12 @@ } } }, - "413": { - "description": "PayloadTooLargeError 413 response", + "423": { + "description": "ResourceSuspendedError 423 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" } } } @@ -397,16 +440,6 @@ } } }, - "429": { - "description": "TooManyRequestsError 429 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" - } - } - } - }, "500": { "description": "UnexpectedError 500 response", "content": { @@ -417,25 +450,13 @@ } } } - }, - "tags": [ - "Long-Term Memory" - ] + } } }, - "/v1/stores/{storeId}/long-term-memory/search": { - "post": { - "description": "Runs a semantic search on long-term memory with filtering options.", - "operationId": "SearchLongTermMemory", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchLongTermMemoryRequestContent" - } - } - } - }, + "/v1/stores/{storeId}/health": { + "get": { + "description": "Returns read-only health for applicable store-scoped RAM features.", + "operationId": "StoreHealth", "parameters": [ { "name": "storeId", @@ -453,11 +474,11 @@ ], "responses": { "200": { - "description": "SearchLongTermMemory 200 response", + "description": "StoreHealth 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchLongTermMemoryResponseContent" + "$ref": "#/components/schemas/StoreHealthResponseContent" } } } @@ -522,6 +543,16 @@ } } }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, "424": { "description": "FailedDependencyError 424 response", "content": { @@ -554,49 +585,54 @@ } }, "tags": [ - "Long-Term Memory" + "Store Health" ] } }, - "/v1/stores/{storeId}/long-term-memory/{memoryId}": { + "/v1/stores/{storeId}/ingestion-hooks/events": { "get": { - "description": "Returns a long-term memory by its ID.", - "operationId": "GetLongTermMemory", + "description": "List hook decisions in stable newest-first order. Pages can be empty after owner filtering; continue while nextCursor is present. Internal run reservations are omitted.", + "operationId": "ListIngestionHookStatuses", "parameters": [ { "name": "storeId", "in": "path", - "description": "The store instance ID.", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The store instance ID." + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." }, "required": true }, { - "name": "memoryId", - "in": "path", - "description": "The memory record ID.", + "name": "limit", + "in": "query", "schema": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "The memory record ID." - }, - "required": true + "type": "integer", + "default": 20, + "maximum": 100, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + } } ], "responses": { "200": { - "description": "GetLongTermMemory 200 response", + "description": "ListIngestionHookStatuses 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetLongTermMemoryResponseContent" + "$ref": "#/components/schemas/ListIngestionHookStatusesResponseContent" } } } @@ -631,118 +667,76 @@ } } }, - "404": { - "description": "NotFoundError 404 response", + "423": { + "description": "ResourceSuspendedError 423 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundErrorResponseContent" + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" } } } }, - "408": { - "description": "TimeoutError 408 response", + "424": { + "description": "MemoryFailedDependencyError 424 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TimeoutErrorResponseContent" + "$ref": "#/components/schemas/MemoryFailedDependencyErrorResponseContent" } } } }, - "413": { - "description": "PayloadTooLargeError 413 response", + "500": { + "description": "UnexpectedError 500 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" - } - } - } - }, - "424": { - "description": "FailedDependencyError 424 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" - } - } - } - }, - "429": { - "description": "TooManyRequestsError 429 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" - } - } - } - }, - "500": { - "description": "UnexpectedError 500 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" } } } } }, "tags": [ - "Long-Term Memory" + "Ingestion Hooks" ] - }, - "patch": { - "description": "Partially updates a long-term memory by its ID.", - "operationId": "UpdateLongTermMemory", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateLongTermMemoryRequestContent" - } - } - } - }, + } + }, + "/v1/stores/{storeId}/ingestion-hooks/events/{eventId}": { + "get": { + "description": "Read a hook decision and application status. Requires authenticated store access and, when enforced, matching owner scope. Content and secrets are never returned.", + "operationId": "GetIngestionHookStatus", "parameters": [ { "name": "storeId", "in": "path", - "description": "The store instance ID.", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The store instance ID." + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." }, "required": true }, { - "name": "memoryId", + "name": "eventId", "in": "path", - "description": "The memory record ID.", "schema": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "The memory record ID." + "type": "string" }, "required": true } ], "responses": { "200": { - "description": "UpdateLongTermMemory 200 response", + "description": "GetIngestionHookStatus 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateLongTermMemoryResponseContent" + "$ref": "#/components/schemas/GetIngestionHookStatusResponseContent" } } } @@ -787,42 +781,22 @@ } } }, - "408": { - "description": "TimeoutError 408 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TimeoutErrorResponseContent" - } - } - } - }, - "413": { - "description": "PayloadTooLargeError 413 response", + "423": { + "description": "ResourceSuspendedError 423 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" } } } }, "424": { - "description": "FailedDependencyError 424 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" - } - } - } - }, - "429": { - "description": "TooManyRequestsError 429 response", + "description": "MemoryFailedDependencyError 424 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + "$ref": "#/components/schemas/MemoryFailedDependencyErrorResponseContent" } } } @@ -839,19 +813,19 @@ } }, "tags": [ - "Long-Term Memory" + "Ingestion Hooks" ] } }, - "/v1/stores/{storeId}/long-term-memory/{memoryId}/fields": { - "patch": { - "description": "Atomically updates a custom-typed long-term memory's text and/or attributes, scoped to the caller's bound memory type. Used by the custom-extraction worker.", - "operationId": "UpdateLongTermMemoryFields", + "/v1/stores/{storeId}/long-term-memory": { + "delete": { + "description": "Deletes long-term memories in bulk by their IDs.", + "operationId": "BulkDeleteLongTermMemories", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateLongTermMemoryFieldsRequestContent" + "$ref": "#/components/schemas/BulkDeleteLongTermMemoriesRequestContent" } } }, @@ -870,24 +844,18 @@ "description": "The store instance ID." }, "required": true - }, - { - "name": "memoryId", - "in": "path", - "description": "The memory record ID.", - "schema": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "The memory record ID." - }, - "required": true } ], "responses": { "200": { - "description": "UpdateLongTermMemoryFields 200 response" + "description": "BulkDeleteLongTermMemories 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkDeleteLongTermMemoriesResponseContent" + } + } + } }, "400": { "description": "BadRequestError 400 response", @@ -939,6 +907,26 @@ } } }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, "424": { "description": "FailedDependencyError 424 response", "content": { @@ -973,12 +961,20 @@ "tags": [ "Long-Term Memory" ] - } - }, - "/v1/stores/{storeId}/session-memory": { - "get": { - "description": "Returns a paginated list of session IDs for a store.", - "operationId": "ListSessions", + }, + "post": { + "description": "Creates long-term memories in bulk. Records without namespace or namespaceRef stay un-namespaced. Automatic routing does not place them in a shared namespace. An explicit namespaceRef must pass destination admission and is never rerouted. Admission failures are reported per record in errors.", + "operationId": "BulkCreateLongTermMemories", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkCreateLongTermMemoriesRequestContent" + } + } + }, + "required": true + }, "parameters": [ { "name": "storeId", @@ -992,58 +988,15 @@ "description": "The store instance ID." }, "required": true - }, - { - "name": "limit", - "in": "query", - "description": "Maximum number of sessions to return. Defaults to 100. Allowed range: 1-1000.", - "schema": { - "type": "integer", - "default": 100, - "maximum": 1000, - "minimum": 1, - "description": "Maximum number of sessions to return. Defaults to 100. Allowed range: 1-1000.", - "format": "int32" - } - }, - { - "name": "pageToken", - "in": "query", - "description": "Opaque token from a previous response for the next page.", - "schema": { - "type": "string", - "description": "Opaque token from a previous response for the next page." - } - }, - { - "name": "filterOwnerId", - "in": "query", - "description": "Filter sessions by owner. Matching is case-sensitive. Mutually exclusive with includeAll.", - "schema": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Filter sessions by owner. Matching is case-sensitive. Mutually exclusive with includeAll." - } - }, - { - "name": "includeAll", - "in": "query", - "description": "Set to true to list all sessions. Required when no filter is given; mutually exclusive with filters.", - "schema": { - "type": "boolean", - "description": "Set to true to list all sessions. Required when no filter is given; mutually exclusive with filters." - } } ], "responses": { - "200": { - "description": "ListSessions 200 response", + "201": { + "description": "BulkCreateLongTermMemories 201 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListSessionsResponseContent" + "$ref": "#/components/schemas/BulkCreateLongTermMemoriesResponseContent" } } } @@ -1108,12 +1061,32 @@ } } }, + "422": { + "description": "IngestionHookStoppedError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestionHookStoppedErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, "424": { - "description": "FailedDependencyError 424 response", + "description": "MemoryFailedDependencyError 424 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + "$ref": "#/components/schemas/MemoryFailedDependencyErrorResponseContent" } } } @@ -1137,22 +1110,32 @@ } } } + }, + "503": { + "description": "IngestionHookRetryError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestionHookRetryErrorResponseContent" + } + } + } } }, "tags": [ - "Session Memory" + "Long-Term Memory" ] } }, - "/v1/stores/{storeId}/session-memory/events": { + "/v1/stores/{storeId}/long-term-memory/move": { "post": { - "description": "Appends a single event to a session. Creates the session if it does not exist. If sessionId is omitted, the server generates one.", - "operationId": "AddSessionEvent", + "description": "Moves long-term memories to an explicit namespaceRef, or automatically routes a memory that is already in a shared namespace when namespaceRef is omitted. Personal and un-namespaced memories are not automatic-routing candidates and keep their current placement with a per-item error. If routing is disabled or no shared destination qualifies, the memory also keeps its current placement and receives a per-item error. An explicit destination must pass admission and is never rerouted. Returns 409 when the explicit destination is archived; automatic routing and admission failures are reported per item in errors.", + "operationId": "MoveLongTermMemories", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AddSessionEventRequestContent" + "$ref": "#/components/schemas/MoveLongTermMemoriesRequestContent" } } }, @@ -1162,24 +1145,24 @@ { "name": "storeId", "in": "path", - "description": "The store instance ID.", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The store instance ID." + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." }, "required": true } ], "responses": { - "201": { - "description": "AddSessionEvent 201 response", + "200": { + "description": "MoveLongTermMemories 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AddSessionEventResponseContent" + "$ref": "#/components/schemas/MoveLongTermMemoriesResponseContent" } } } @@ -1234,6 +1217,16 @@ } } }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, "413": { "description": "PayloadTooLargeError 413 response", "content": { @@ -1244,6 +1237,16 @@ } } }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, "424": { "description": "FailedDependencyError 424 response", "content": { @@ -1276,45 +1279,48 @@ } }, "tags": [ - "Session Memory" + "Long-Term Memory" ] } }, - "/v1/stores/{storeId}/session-memory/{sessionId}": { - "delete": { - "description": "Deletes the session memory for a session.", - "operationId": "DeleteSessionMemory", - "parameters": [ - { - "name": "storeId", - "in": "path", - "description": "The store instance ID.", - "schema": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "The store instance ID." - }, - "required": true - }, + "/v1/stores/{storeId}/long-term-memory/search": { + "post": { + "description": "Runs a semantic search on long-term memory with filtering options.", + "operationId": "SearchLongTermMemory", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchLongTermMemoryRequestContent" + } + } + } + }, + "parameters": [ { - "name": "sessionId", + "name": "storeId", "in": "path", - "description": "The session ID.", + "description": "The store instance ID.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The session ID." + "description": "The store instance ID." }, "required": true } ], "responses": { - "204": { - "description": "DeleteSessionMemory 204 response" + "200": { + "description": "SearchLongTermMemory 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchLongTermMemoryResponseContent" + } + } + } }, "400": { "description": "BadRequestError 400 response", @@ -1376,12 +1382,22 @@ } } }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, "424": { - "description": "FailedDependencyError 424 response", + "description": "MemoryFailedDependencyError 424 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + "$ref": "#/components/schemas/MemoryFailedDependencyErrorResponseContent" } } } @@ -1408,12 +1424,14 @@ } }, "tags": [ - "Session Memory" + "Long-Term Memory" ] - }, + } + }, + "/v1/stores/{storeId}/long-term-memory/{memoryId}": { "get": { - "description": "Returns the session memory for a session.", - "operationId": "GetSessionMemory", + "description": "Returns a long-term memory by its ID.", + "operationId": "GetLongTermMemory", "parameters": [ { "name": "storeId", @@ -1429,35 +1447,26 @@ "required": true }, { - "name": "sessionId", + "name": "memoryId", "in": "path", - "description": "The session ID.", + "description": "The memory record ID.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The session ID." + "description": "The memory record ID." }, "required": true - }, - { - "name": "includeSummarisedEvents", - "in": "query", - "description": "When true, the response includes events that have already been covered by the session summary and have not yet been cleaned up. This flag is not expected to be set under normal usage.", - "schema": { - "type": "boolean", - "description": "When true, the response includes events that have already been covered by the session summary and have not yet been cleaned up. This flag is not expected to be set under normal usage." - } } ], "responses": { "200": { - "description": "GetSessionMemory 200 response", + "description": "GetLongTermMemory 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetSessionMemoryResponseContent" + "$ref": "#/components/schemas/GetLongTermMemoryResponseContent" } } } @@ -1522,6 +1531,16 @@ } } }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, "424": { "description": "FailedDependencyError 424 response", "content": { @@ -1554,14 +1573,21 @@ } }, "tags": [ - "Session Memory" + "Long-Term Memory" ] - } - }, - "/v1/stores/{storeId}/session-memory/{sessionId}/events/{eventId}": { - "delete": { - "description": "Deletes a single event from a session by event ID.", - "operationId": "DeleteSessionEvent", + }, + "patch": { + "description": "Partially updates a long-term memory by its ID. The merged record must pass admission at its destination. Updates never select a different destination automatically.", + "operationId": "UpdateLongTermMemory", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateLongTermMemoryRequestContent" + } + } + } + }, "parameters": [ { "name": "storeId", @@ -1577,35 +1603,29 @@ "required": true }, { - "name": "sessionId", + "name": "memoryId", "in": "path", - "description": "The session ID.", + "description": "The memory record ID.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The session ID." - }, - "required": true - }, - { - "name": "eventId", - "in": "path", - "description": "The event ID.", - "schema": { - "type": "string", - "maxLength": 32, - "minLength": 32, - "pattern": "^[a-zA-Z0-9]+$", - "description": "The event ID." + "description": "The memory record ID." }, "required": true } ], "responses": { - "204": { - "description": "DeleteSessionEvent 204 response" + "200": { + "description": "UpdateLongTermMemory 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateLongTermMemoryResponseContent" + } + } + } }, "400": { "description": "BadRequestError 400 response", @@ -1657,6 +1677,16 @@ } } }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, "413": { "description": "PayloadTooLargeError 413 response", "content": { @@ -1667,12 +1697,22 @@ } } }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, "424": { - "description": "FailedDependencyError 424 response", + "description": "MemoryFailedDependencyError 424 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + "$ref": "#/components/schemas/MemoryFailedDependencyErrorResponseContent" } } } @@ -1699,12 +1739,24 @@ } }, "tags": [ - "Session Memory" + "Long-Term Memory" ] - }, - "get": { - "description": "Returns a single event from a session by event ID.", - "operationId": "GetSessionEvent", + } + }, + "/v1/stores/{storeId}/long-term-memory/{memoryId}/fields": { + "patch": { + "description": "Atomically updates a custom-typed long-term memory's text and/or attributes, scoped to the caller's bound memory type. Used by the custom-extraction worker. New text must pass destination admission before the update is applied.", + "operationId": "UpdateLongTermMemoryFields", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateLongTermMemoryFieldsRequestContent" + } + } + }, + "required": true + }, "parameters": [ { "name": "storeId", @@ -1720,42 +1772,22 @@ "required": true }, { - "name": "sessionId", + "name": "memoryId", "in": "path", - "description": "The session ID.", + "description": "The memory record ID.", "schema": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The session ID." - }, - "required": true - }, - { - "name": "eventId", - "in": "path", - "description": "The event ID.", - "schema": { - "type": "string", - "maxLength": 32, - "minLength": 32, - "pattern": "^[a-zA-Z0-9]+$", - "description": "The event ID." + "description": "The memory record ID." }, "required": true } ], "responses": { "200": { - "description": "GetSessionEvent 200 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetSessionEventResponseContent" - } - } - } + "description": "UpdateLongTermMemoryFields 200 response" }, "400": { "description": "BadRequestError 400 response", @@ -1807,22 +1839,32 @@ } } }, - "413": { - "description": "PayloadTooLargeError 413 response", + "409": { + "description": "ConflictError 409 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" } } } }, "424": { - "description": "FailedDependencyError 424 response", + "description": "MemoryFailedDependencyError 424 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + "$ref": "#/components/schemas/MemoryFailedDependencyErrorResponseContent" } } } @@ -1849,151 +1891,5668 @@ } }, "tags": [ - "Session Memory" + "Long-Term Memory" ] } - } - }, - "components": { - "schemas": { - "AddSessionEventRequestContent": { - "type": "object", - "properties": { - "sessionId": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Optional session ID. If omitted, the server generates a unique ID." + }, + "/v1/stores/{storeId}/long-term-memory/{memoryId}/sources": { + "get": { + "description": "Pages source links for one long-term memory. Tokens bind this query and expire.", + "operationId": "ListMemorySourceLinks", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true }, - "actorId": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Unique actor identifier (1-255 chars). Can represent a user, agent, or any participant." + { + "name": "memoryId", + "in": "path", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + }, + "required": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "pageToken", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ListMemorySourceLinks 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListMemorySourceLinksResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Long-Term Memory" + ] + } + }, + "/v1/stores/{storeId}/long-term-memory/{memoryId}/sources/{sourceId}/evidence": { + "get": { + "description": "Pages extraction evidence for one source of a memory, in append order. Tokens bind the store, current owner, memory and source, and expire.", + "operationId": "ListMemorySourceEvidence", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "memoryId", + "in": "path", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + }, + "required": true + }, + { + "name": "sourceId", + "in": "path", + "description": "Stable source identifier for one revisioned origin in a store.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Stable source identifier for one revisioned origin in a store." + }, + "required": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "pageToken", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ListMemorySourceEvidence 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListMemorySourceEvidenceResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Long-Term Memory" + ] + } + }, + "/v1/stores/{storeId}/namespace-routing": { + "get": { + "description": "Returns the store's namespace routing policy. An empty disabled policy is returned when none has been saved.", + "operationId": "GetNamespaceRouting", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetNamespaceRouting 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNamespaceRoutingResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Namespaces" + ] + }, + "put": { + "description": "Replaces the store's namespace routing policy. Namespaces remain hierarchy records; routing is an ordered store-level document of plug-in strategies.", + "operationId": "PutNamespaceRouting", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutNamespaceRoutingRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "PutNamespaceRouting 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutNamespaceRoutingResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Namespaces" + ] + } + }, + "/v1/stores/{storeId}/namespaces": { + "get": { + "description": "Lists namespace roots or direct children.", + "operationId": "ListNamespaces", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "parentId", + "in": "query", + "description": "List direct children of this namespace. Omit to list roots.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "List direct children of this namespace. Omit to list roots." + } + }, + { + "name": "scope", + "in": "query", + "description": "Restrict root results to one scope. Cannot be combined with parentId.", + "schema": { + "$ref": "#/components/schemas/NamespaceScope" + } + }, + { + "name": "ownerId", + "in": "query", + "description": "Restrict personal root results to an owner. Cannot be combined with parentId; ignored for shared roots.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Restrict personal root results to an owner. Cannot be combined with parentId; ignored for shared roots." + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Field to sort by. Defaults to id. Name sorts by the indexed display name using Redis text collation, not the namespace path. Equal values are ordered by ID ascending.", + "schema": { + "$ref": "#/components/schemas/NamespaceSortBy" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort direction. Defaults to asc. Use the same sort options for each page; concurrent changes can shift offset-based pages.", + "schema": { + "$ref": "#/components/schemas/NamespaceSortOrder" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of namespaces to return. Defaults to 100. Allowed range: 1-1000.", + "schema": { + "type": "integer", + "default": 100, + "maximum": 1000, + "minimum": 1, + "description": "Maximum number of namespaces to return. Defaults to 100. Allowed range: 1-1000.", + "format": "int32" + } + }, + { + "name": "pageToken", + "in": "query", + "description": "Opaque token from a previous response for the next page.", + "schema": { + "type": "string", + "description": "Opaque token from a previous response for the next page." + } + } + ], + "responses": { + "200": { + "description": "ListNamespaces 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListNamespacesResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Namespaces" + ] + }, + "post": { + "description": "Creates a namespace resource as a root, direct child, or explicit hierarchy path. Returns 409 when an active or archived namespace already occupies the requested location.", + "operationId": "CreateNamespace", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNamespaceRequestContent" + } + } + } + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "CreateNamespace 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNamespaceResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Namespaces" + ] + } + }, + "/v1/stores/{storeId}/namespaces/{namespaceId}": { + "delete": { + "description": "Deletes an empty leaf namespace. Returns 409 when the namespace has children or memory placements, or when it changed concurrently.", + "operationId": "DeleteNamespace", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "namespaceId", + "in": "path", + "description": "Server-generated namespace identifier.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated namespace identifier." + }, + "required": true + } + ], + "responses": { + "204": { + "description": "DeleteNamespace 204 response" + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Namespaces" + ] + }, + "get": { + "description": "Gets one namespace by its canonical ID.", + "operationId": "GetNamespace", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "namespaceId", + "in": "path", + "description": "Server-generated namespace identifier.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated namespace identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetNamespace 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNamespaceResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Namespaces" + ] + }, + "patch": { + "description": "Renames or archives a namespace. Returns 409 when the namespace changed concurrently, an active or archived sibling already uses the requested name, or the namespace is archived.", + "operationId": "UpdateNamespace", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateNamespaceRequestContent" + } + } + } + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "namespaceId", + "in": "path", + "description": "Server-generated namespace identifier.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated namespace identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateNamespace 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateNamespaceResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Namespaces" + ] + } + }, + "/v1/stores/{storeId}/session-memory": { + "get": { + "description": "Returns a paginated list of session IDs for a store.", + "operationId": "ListSessions", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "The store instance ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The store instance ID." + }, + "required": true + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of sessions to return. Defaults to 100. Allowed range: 1-1000.", + "schema": { + "type": "integer", + "default": 100, + "maximum": 1000, + "minimum": 1, + "description": "Maximum number of sessions to return. Defaults to 100. Allowed range: 1-1000.", + "format": "int32" + } + }, + { + "name": "pageToken", + "in": "query", + "description": "Opaque token from a previous response for the next page.", + "schema": { + "type": "string", + "description": "Opaque token from a previous response for the next page." + } + }, + { + "name": "filterOwnerId", + "in": "query", + "description": "Filter sessions by owner. The owner of a session is the actorId of its first event. Matching is exact and case-sensitive. Mutually exclusive with includeAll.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Filter sessions by owner. The owner of a session is the actorId of its first event. Matching is exact and case-sensitive. Mutually exclusive with includeAll." + } + }, + { + "name": "namespaceRef", + "in": "query", + "description": "Filter sessions by stable namespace ID. Matching is exact and case-sensitive, excludes deprecated namespace labels and sessions without a namespace resource, and does not include descendants. Combined with filterOwnerId using AND. Mutually exclusive with includeAll.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Filter sessions by stable namespace ID. Matching is exact and case-sensitive, excludes deprecated namespace labels and sessions without a namespace resource, and does not include descendants. Combined with filterOwnerId using AND. Mutually exclusive with includeAll." + } + }, + { + "name": "includeAll", + "in": "query", + "description": "Set to true to list all sessions. Required when no filter is given; mutually exclusive with filters.", + "schema": { + "type": "boolean", + "description": "Set to true to list all sessions. Required when no filter is given; mutually exclusive with filters." + } + } + ], + "responses": { + "200": { + "description": "ListSessions 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSessionsResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Session Memory" + ] + } + }, + "/v1/stores/{storeId}/session-memory/events": { + "post": { + "description": "Appends a single event to a session. Creates the session if it does not exist. If sessionId is omitted, the server generates one.", + "operationId": "AddSessionEvent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddSessionEventRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "The store instance ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The store instance ID." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "AddSessionEvent 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddSessionEventResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "422": { + "description": "IngestionHookStoppedError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestionHookStoppedErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "IngestionHookRetryError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestionHookRetryErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Session Memory" + ] + } + }, + "/v1/stores/{storeId}/session-memory/{sessionId}": { + "delete": { + "description": "Deletes the session memory for a session.", + "operationId": "DeleteSessionMemory", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "The store instance ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The store instance ID." + }, + "required": true + }, + { + "name": "sessionId", + "in": "path", + "description": "The session ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The session ID." + }, + "required": true + } + ], + "responses": { + "204": { + "description": "DeleteSessionMemory 204 response" + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Session Memory" + ] + }, + "get": { + "description": "Returns the session memory for a session.", + "operationId": "GetSessionMemory", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "The store instance ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The store instance ID." + }, + "required": true + }, + { + "name": "sessionId", + "in": "path", + "description": "The session ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The session ID." + }, + "required": true + }, + { + "name": "includeSummarisedEvents", + "in": "query", + "description": "When true, the response includes events that have already been covered by the session summary and have not yet been cleaned up. This flag is not expected to be set under normal usage.", + "schema": { + "type": "boolean", + "description": "When true, the response includes events that have already been covered by the session summary and have not yet been cleaned up. This flag is not expected to be set under normal usage." + } + } + ], + "responses": { + "200": { + "description": "GetSessionMemory 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSessionMemoryResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Session Memory" + ] + } + }, + "/v1/stores/{storeId}/session-memory/{sessionId}/events/{eventId}": { + "delete": { + "description": "Deletes a single event from a session by event ID.", + "operationId": "DeleteSessionEvent", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "The store instance ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The store instance ID." + }, + "required": true + }, + { + "name": "sessionId", + "in": "path", + "description": "The session ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The session ID." + }, + "required": true + }, + { + "name": "eventId", + "in": "path", + "description": "The event ID.", + "schema": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "The event ID." + }, + "required": true + } + ], + "responses": { + "204": { + "description": "DeleteSessionEvent 204 response" + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Session Memory" + ] + }, + "get": { + "description": "Returns a single event from a session by event ID.", + "operationId": "GetSessionEvent", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "The store instance ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The store instance ID." + }, + "required": true + }, + { + "name": "sessionId", + "in": "path", + "description": "The session ID.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The session ID." + }, + "required": true + }, + { + "name": "eventId", + "in": "path", + "description": "The event ID.", + "schema": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "The event ID." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetSessionEvent 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSessionEventResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Session Memory" + ] + } + }, + "/v1/stores/{storeId}/sessions": { + "get": { + "description": "Returns paginated session rows with metadata and caller-selected order. The ID-only ListSessions operation remains unchanged.", + "operationId": "ListSessionSummaries", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of sessions to return. Defaults to 100. Allowed range: 1-1000.", + "schema": { + "type": "integer", + "default": 100, + "maximum": 1000, + "minimum": 1, + "description": "Maximum number of sessions to return. Defaults to 100. Allowed range: 1-1000.", + "format": "int32" + } + }, + { + "name": "pageToken", + "in": "query", + "description": "Opaque token from a previous response for the next page. Bound to the effective filters and sort.", + "schema": { + "type": "string", + "description": "Opaque token from a previous response for the next page. Bound to the effective filters and sort." + } + }, + { + "name": "filterOwnerId", + "in": "query", + "description": "Filter sessions by owner. The owner of a session is the actorId of its first event. Matching is exact and case-sensitive. Combined with namespaceRef using AND. Mutually exclusive with includeAll.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Filter sessions by owner. The owner of a session is the actorId of its first event. Matching is exact and case-sensitive. Combined with namespaceRef using AND. Mutually exclusive with includeAll." + } + }, + { + "name": "namespaceRef", + "in": "query", + "description": "Filter by stable namespace ID. Combined with filterOwnerId using AND. Mutually exclusive with includeAll.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Filter by stable namespace ID. Combined with filterOwnerId using AND. Mutually exclusive with includeAll." + } + }, + { + "name": "includeAll", + "in": "query", + "description": "Set to true to list all visible sessions. Required when no filter is given; mutually exclusive with filters.", + "schema": { + "type": "boolean", + "description": "Set to true to list all visible sessions. Required when no filter is given; mutually exclusive with filters." + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Sort field. Defaults to updatedAt.", + "schema": { + "$ref": "#/components/schemas/SessionListSortBy" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort direction. Defaults to desc for timestamps and asc for sessionId.", + "schema": { + "$ref": "#/components/schemas/SessionListSortOrder" + } + } + ], + "responses": { + "200": { + "description": "ListSessionSummaries 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSessionSummariesResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Session Memory" + ] + } + }, + "/v1/stores/{storeId}/sources/{sourceId}/memories": { + "get": { + "description": "Pages memories linked to one source. A source link does not grant access to a memory in another owner or namespace.", + "operationId": "ListSourceMemories", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "sourceId", + "in": "path", + "description": "Stable source identifier for one revisioned origin in a store.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Stable source identifier for one revisioned origin in a store." + }, + "required": true + }, + { + "name": "ownerId", + "in": "query", + "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants." + }, + "required": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "pageToken", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ListSourceMemories 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSourceMemoriesResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Long-Term Memory" + ] + } + }, + "/v1/stores/{storeId}/summary-views": { + "get": { + "description": "Lists Summary View definitions and high-level state. Partition values are not included.", + "operationId": "ListSummaryViews", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 100, + "maximum": 100, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "pageToken", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ListSummaryViews 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSummaryViewsResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Summary Views" + ] + }, + "post": { + "description": "Creates a Summary View definition and asynchronously enqueues its initial reconciliation.", + "operationId": "CreateSummaryView", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSummaryViewRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "CreateSummaryView 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSummaryViewResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "SummaryViewConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SummaryViewConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "SummaryViewQuotaError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SummaryViewQuotaErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Summary Views" + ] + } + }, + "/v1/stores/{storeId}/summary-views/{viewId}": { + "delete": { + "description": "Marks a Summary View deleting and asynchronously removes its schedule and materializations in bounded batches. Repeated requests are idempotent.", + "operationId": "DeleteSummaryView", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "viewId", + "in": "path", + "description": "Server-generated Summary View identifier.", + "schema": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "required": true + } + ], + "responses": { + "202": { + "description": "DeleteSummaryView 202 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteSummaryViewResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "SummaryViewConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SummaryViewConflictErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Summary Views" + ] + }, + "get": { + "description": "Returns one Summary View definition without partition values.", + "operationId": "GetSummaryView", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "viewId", + "in": "path", + "description": "Server-generated Summary View identifier.", + "schema": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetSummaryView 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSummaryViewResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Summary Views" + ] + }, + "patch": { + "description": "Updates mutable Summary View fields. Source is immutable. Material changes increment definitionVersion; name- or description-only changes do not enqueue refresh work.", + "operationId": "UpdateSummaryView", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSummaryViewRequestContent" + } + } + } + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "viewId", + "in": "path", + "description": "Server-generated Summary View identifier.", + "schema": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateSummaryView 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSummaryViewResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "SummaryViewConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SummaryViewConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "SummaryViewQuotaError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SummaryViewQuotaErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Summary Views" + ] + } + }, + "/v1/stores/{storeId}/summary-views/{viewId}/refresh": { + "post": { + "description": "Asynchronously enqueues one exact partition refresh or a complete view reconciliation. It never performs generation synchronously.", + "operationId": "RefreshSummaryView", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshSummaryViewRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "viewId", + "in": "path", + "description": "Server-generated Summary View identifier.", + "schema": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "required": true + } + ], + "responses": { + "202": { + "description": "RefreshSummaryView 202 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshSummaryViewResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "SummaryViewConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SummaryViewConflictErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "SummaryViewQuotaError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SummaryViewQuotaErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Summary Views" + ] + } + }, + "/v1/stores/{storeId}/summary-views/{viewId}/result": { + "get": { + "description": "Returns one authorized exact Summary View partition after validating its contributing source records. Invalid output is withheld while repair is pending.", + "operationId": "GetSummaryViewResult", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated store IDs are typically 32-character UUIDs without dashes." + }, + "required": true + }, + { + "name": "viewId", + "in": "path", + "description": "Server-generated Summary View identifier.", + "schema": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "required": true + }, + { + "name": "ownerId", + "in": "query", + "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants." + } + }, + { + "name": "sessionId", + "in": "query", + "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." + } + }, + { + "name": "namespaceRef", + "in": "query", + "description": "Server-generated namespace identifier.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated namespace identifier." + } + }, + { + "name": "memoryType", + "in": "query", + "description": "Identifier of a long-term memory type.\n\nBuilt-in types include `semantic`, `episodic`, and `message`.\nStores may also register custom memory types; this open string accepts any\nregistered type name.\n\nConstraints: 1-64 chars, must start with a letter, followed by letters, digits,\nunderscores, or dashes.\n", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Identifier of a long-term memory type.\n\nBuilt-in types include `semantic`, `episodic`, and `message`.\nStores may also register custom memory types; this open string accepts any\nregistered type name.\n\nConstraints: 1-64 chars, must start with a letter, followed by letters, digits,\nunderscores, or dashes." + } + }, + { + "name": "includeSourceMemoryIds", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "GetSummaryViewResult 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSummaryViewResultResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "SummaryViewConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SummaryViewConflictErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Summary Views" + ] + } + } + }, + "components": { + "schemas": { + "AddSessionEventRequestContent": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "maxLength": 128, + "minLength": 1, + "pattern": "^[ -~]+$", + "description": "Required when the store selects session.append input hooks. Reuse this key only for an identical request; accepted retries reuse the saved decision and event identity." + }, + "sessionId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Optional session ID. If omitted, the server generates a unique ID." + }, + "actorId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Unique actor identifier (1-255 chars). Can represent a user, agent, or any participant." + }, + "namespace": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Optional deprecated namespace label for the session. This label does not create or resolve a namespace resource. Do not supply it with namespaceRef.\n\nLike `ownerId`, this is a session-level value taken from the first event: the value sent with the first event of a session is stored on the session, and values sent with later events are ignored. Omit it to leave extracted memories without a namespace.", + "deprecated": true + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRefInput" + }, + "role": { + "$ref": "#/components/schemas/MessageRole" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Content" + }, + "description": "The message content as an array of typed content parts." + }, + "createdAt": { + "type": "string", + "description": "Client-supplied timestamp for when the event happened (UTC).", + "format": "date-time" + }, + "metadata": { + "description": "Optional metadata as any valid JSON value." + } + }, + "required": [ + "actorId", + "content", + "createdAt", + "role" + ] + }, + "AddSessionEventResponseContent": { + "type": "object", + "description": "Response returned after appending a session event.", + "properties": { + "event": { + "$ref": "#/components/schemas/SessionEvent" + } + }, + "required": [ + "event" + ] + }, + "AttributeFilterClause": { + "description": "Filter clause for a single record attribute. Exactly one member is set; the member must match the attribute's declared field type.", + "oneOf": [ + { + "type": "object", + "title": "string", + "properties": { + "string": { + "type": "string", + "description": "Match a string attribute equal to this value." + } + }, + "required": [ + "string" + ] + }, + { + "type": "object", + "title": "number", + "properties": { + "number": { + "type": "number", + "description": "Match a numeric attribute equal to this value.", + "format": "double" + } + }, + "required": [ + "number" + ] + }, + { + "type": "object", + "title": "boolean", + "properties": { + "boolean": { + "type": "boolean", + "description": "Match a boolean attribute equal to this value." + } + }, + "required": [ + "boolean" + ] + }, + { + "type": "object", + "title": "list", + "properties": { + "list": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Match a list attribute that contains all of these values." + } + }, + "required": [ + "list" + ] + } + ] + }, + "AttributeFilterMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AttributeFilterClause" + }, + "description": "Per-attribute filter clauses keyed by the custom memory type's field name." + }, + "AttributeMap": { + "type": "object", + "additionalProperties": {}, + "description": "Structured record attributes: a map from the custom memory type's field name to that field's value. Values are arbitrary JSON matching the field's declared type (str, int, float, bool, list[str], list[float], or object)." + }, + "AuthenticationErrorResponseContent": { + "type": "object", + "description": "Authentication credentials are missing, malformed, or invalid.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 401, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/AuthenticationErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "AuthenticationErrorType": { + "type": "string", + "description": "Problem type URI for authentication errors.", + "enum": [ + "/errors/authentication-failed" + ] + }, + "BadRequestErrorResponseContent": { + "type": "object", + "description": "Request validation or input decoding failed.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 400, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/BadRequestErrorType" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldError" + }, + "description": "Optional field-level validation errors." + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "BadRequestErrorType": { + "type": "string", + "description": "Problem type URI for bad request errors.", + "enum": [ + "/errors/invalid-data" + ] + }, + "BulkCreateLongTermMemoriesRequestContent": { + "type": "object", + "description": "Bulk create request for long-term memory records.", + "properties": { + "idempotencyKey": { + "type": "string", + "maxLength": 128, + "minLength": 1, + "pattern": "^[ -~]+$", + "description": "Required when the memory.create input hook is selected. Reuse the same key and original request to resume its saved decision." + }, + "memories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateMemoryRecord" + }, + "maxItems": 100, + "minItems": 1, + "description": "List of memory records to create." + } + }, + "required": [ + "memories" + ] + }, + "BulkCreateLongTermMemoriesResponseContent": { + "type": "object", + "description": "Bulk create result for long-term memory records.", + "properties": { + "created": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + }, + "description": "IDs of successfully created memory records. Always returned; empty when no records were created." + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BulkOperationError" + }, + "description": "Errors for memory records that failed to be created." + } + }, + "required": [ + "created" + ] + }, + "BulkDeleteLongTermMemoriesRequestContent": { + "type": "object", + "description": "Bulk delete request for long-term memory records.", + "properties": { + "memoryIds": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + }, + "maxItems": 100, + "minItems": 1, + "description": "List of memory record IDs to delete (1-100 IDs)." + } + }, + "required": [ + "memoryIds" + ] + }, + "BulkDeleteLongTermMemoriesResponseContent": { + "type": "object", + "description": "Bulk delete result for long-term memory records.", + "properties": { + "deleted": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + }, + "description": "IDs of successfully deleted memory records. Always returned; empty when no records were deleted." + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BulkOperationError" + }, + "description": "Errors for memory records that failed to be deleted." + } + }, + "required": [ + "deleted" + ] + }, + "BulkOperationError": { + "type": "object", + "description": "Error detail for a single item in a bulk operation.", + "properties": { + "id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record ID of the item that failed." + }, + "error": { + "type": "string", + "description": "Error message describing the failure." + } + }, + "required": [ + "error", + "id" + ] + }, + "ConflictErrorResponseContent": { + "type": "object", + "description": "The request conflicts with the current state of the resource.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 409, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ConflictErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "ConflictErrorType": { + "type": "string", + "description": "Problem type URI for conflict errors.", + "enum": [ + "/errors/conflict" + ] + }, + "Content": { + "description": "A single content item in a message. Exactly one member is set.", + "oneOf": [ + { + "type": "object", + "title": "text", + "properties": { + "text": { + "type": "string", + "description": "Text content." + } + }, + "required": [ + "text" + ] + } + ] + }, + "CreateDocumentIngestionRequestContent": { + "type": "object", + "properties": { + "documentId": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Customer-controlled logical document ID. Opaque and case-sensitive." + }, + "revision": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Customer-controlled immutable revision token. Opaque and case-sensitive." + }, + "content": { + "type": "string", + "format": "byte" + }, + "mediaType": { + "$ref": "#/components/schemas/DocumentMediaType" + }, + "ownerId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants." + }, + "extractorId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Extractor identifier (1-64 chars, alphanumeric and dashes)." + }, + "idempotencyKey": { + "type": "string", + "maxLength": 128, + "minLength": 1, + "description": "Customer idempotency key for one document submission." + } + }, + "required": [ + "content", + "documentId", + "extractorId", + "idempotencyKey", + "mediaType", + "ownerId", + "revision" + ] + }, + "CreateDocumentIngestionResponseContent": { + "type": "object", + "properties": { + "ingestionId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated document ingestion identifier." + }, + "status": { + "$ref": "#/components/schemas/DocumentIngestionStatus" + } + }, + "required": [ + "ingestionId", + "status" + ] + }, + "CreateMemoryRecord": { + "type": "object", + "description": "A memory record to create.", + "properties": { + "id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Client-provided memory record ID for idempotent creation." + }, + "text": { + "type": "string", + "maxLength": 50000, + "minLength": 1, + "description": "The memory content (1-50000 chars)." + }, + "memoryType": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Type of memory. Built-in or a custom type registered on the store." + }, + "sessionId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Session ID associated with this memory (1-64 chars, alphanumeric and dashes)." + }, + "ownerId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Owner ID associated with this memory (1-64 chars, alphanumeric and dashes)." + }, + "namespace": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Optional deprecated namespace label. It does not create or resolve a namespace resource. Do not supply it with namespaceRef. If both fields are omitted, the record is created without a namespace. Automatic routing does not place an un-namespaced record in a shared namespace.", + "deprecated": true + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRefInput" + }, + "topics": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "description": "Topic name (1-100 chars)." + }, + "maxItems": 50, + "description": "Topic tags for categorization (max 50 topics, each 1-100 chars)." + }, + "attributes": { + "$ref": "#/components/schemas/AttributeMap" + } + }, + "required": [ + "id", + "ownerId", + "text" + ] + }, + "CreateNamespaceRequestContent": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "One namespace segment. Use with scope for a root or parentId for a child." + }, + "path": { + "type": "string", + "maxLength": 1024, + "minLength": 1, + "description": "A complete hierarchy path to create. Missing segments are created as needed. Mutually exclusive with name and parentId. The path does not encode scope." + }, + "scope": { + "$ref": "#/components/schemas/NamespaceScope" + }, + "parentId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Parent namespace for a direct child." + }, + "ownerId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Owner for a personal root or path. Required for personal scope; omit for shared roots and children." + } + } + }, + "CreateNamespaceResponseContent": { + "type": "object", + "properties": { + "namespace": { + "$ref": "#/components/schemas/Namespace" + } + }, + "required": [ + "namespace" + ] + }, + "CreateSummaryViewRequestContent": { + "type": "object", + "properties": { + "visibility": { + "$ref": "#/components/schemas/SummaryViewVisibility" + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "description": "Customer-visible Summary View name (1-100 characters)." + }, + "description": { + "type": "string", + "maxLength": 200, + "description": "Optional Summary View description (up to 200 characters)." + }, + "source": { + "$ref": "#/components/schemas/SummaryViewSource" + }, + "filter": { + "$ref": "#/components/schemas/LongTermMemoryFilter" + }, + "filterOp": { + "$ref": "#/components/schemas/FilterConjunction" + }, + "groupBy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SummaryViewGroupField" + }, + "maxItems": 4 + }, + "evictionPolicy": { + "$ref": "#/components/schemas/SummaryViewEvictionPolicy" + }, + "timeWindowSeconds": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "output": { + "$ref": "#/components/schemas/SummaryViewOutputConfig" + }, + "refreshPolicy": { + "$ref": "#/components/schemas/SummaryViewRefreshPolicy" + } + }, + "required": [ + "groupBy", + "name", + "output", + "refreshPolicy", + "source" + ] + }, + "CreateSummaryViewResponseContent": { + "type": "object", + "description": "An owned Summary View definition. Results are separate derived state and never enter long-term-memory search.", + "properties": { + "id": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "ownerId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Immutable canonical user ID assigned by the server. Clients cannot set ownership." + }, + "visibility": { + "$ref": "#/components/schemas/SummaryViewVisibility" + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "description": "Customer-visible Summary View name (1-100 characters)." + }, + "description": { + "type": "string", + "maxLength": 200, + "description": "Optional Summary View description (up to 200 characters)." + }, + "source": { + "$ref": "#/components/schemas/SummaryViewSource" + }, + "filter": { + "$ref": "#/components/schemas/LongTermMemoryFilter" + }, + "filterOp": { + "$ref": "#/components/schemas/FilterConjunction" + }, + "groupBy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SummaryViewGroupField" + }, + "maxItems": 4 + }, + "evictionPolicy": { + "$ref": "#/components/schemas/SummaryViewEvictionPolicy" + }, + "timeWindowSeconds": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "output": { + "$ref": "#/components/schemas/SummaryViewOutputConfig" + }, + "refreshPolicy": { + "$ref": "#/components/schemas/SummaryViewRefreshPolicy" + }, + "definitionVersion": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "state": { + "$ref": "#/components/schemas/SummaryViewState" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "definitionVersion", + "filterOp", + "groupBy", + "id", + "name", + "output", + "ownerId", + "refreshPolicy", + "source", + "state", + "updatedAt", + "visibility" + ] + }, + "CreatedAtFilter": { + "type": "object", + "description": "Filter by creation timestamp.", + "properties": { + "gt": { + "type": "string", + "description": "After the given timestamp.", + "format": "date-time" + }, + "gte": { + "type": "string", + "description": "At or after the given timestamp.", + "format": "date-time" + }, + "lt": { + "type": "string", + "description": "Before the given timestamp.", + "format": "date-time" + }, + "lte": { + "type": "string", + "description": "At or before the given timestamp.", + "format": "date-time" + }, + "eq": { + "type": "string", + "description": "Exactly at the given timestamp.", + "format": "date-time" + } + } + }, + "DeleteSummaryViewResponseContent": { + "type": "object", + "properties": { + "viewId": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "state": { + "$ref": "#/components/schemas/SummaryViewState" + } + }, + "required": [ + "state", + "viewId" + ] + }, + "DocumentIngestionCleanupState": { + "type": "string", + "enum": [ + "pending", + "deleted", + "failed" + ] + }, + "DocumentIngestionProcessingState": { + "type": "string", + "enum": [ + "queued", + "running", + "succeeded", + "failed", + "partial" + ] + }, + "DocumentIngestionRecord": { + "type": "object", + "properties": { + "ingestionId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated document ingestion identifier." + }, + "status": { + "$ref": "#/components/schemas/DocumentIngestionStatus" + } + }, + "required": [ + "ingestionId", + "status" + ] + }, + "DocumentIngestionStatus": { + "type": "object", + "properties": { + "processing": { + "$ref": "#/components/schemas/DocumentIngestionProcessingState" + }, + "cleanup": { + "$ref": "#/components/schemas/DocumentIngestionCleanupState" + }, + "extractorId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Extractor identifier (1-64 chars, alphanumeric and dashes)." + }, + "extractorRevision": { + "type": "integer", + "format": "int64" + }, + "completedChunks": { + "type": "integer", + "format": "int32" + }, + "totalChunks": { + "type": "integer", + "format": "int32" + }, + "createdCount": { + "type": "integer", + "format": "int32" + }, + "filteredCount": { + "type": "integer", + "format": "int32" + }, + "error": { + "type": "string" + }, + "retentionDeadline": { + "type": "number", + "format": "double" + }, + "deletedAt": { + "type": "number", + "format": "double" + } + }, + "required": [ + "cleanup", + "processing" + ] + }, + "DocumentMediaType": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown" + ] + }, + "DocumentSourceIdentity": { + "type": "object", + "properties": { + "documentId": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Customer-controlled logical document ID. Opaque and case-sensitive." + }, + "revision": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Customer-controlled immutable revision token. Opaque and case-sensitive." + } + }, + "required": [ + "documentId", + "revision" + ] + }, + "EvidenceProducer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 128, + "minLength": 1 + } + }, + "required": [ + "name" + ] + }, + "FailedDependencyErrorResponseContent": { + "type": "object", + "description": "A dependent resource required to process the request is unavailable or unhealthy.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 424, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/FailedDependencyErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "FailedDependencyErrorType": { + "type": "string", + "description": "Problem type URI for failed-dependency errors.", + "enum": [ + "/errors/resource-unavailable", + "/errors/database-out-of-memory" + ] + }, + "FieldError": { + "type": "object", + "description": "Validation error details for a single request field.", + "properties": { + "field": { + "type": "string", + "description": "Name of the invalid request field." + }, + "rule": { + "type": "string", + "description": "Validation rule that was violated." + }, + "message": { + "type": "string", + "description": "Human-readable validation error message." + } + }, + "required": [ + "field", + "message", + "rule" + ] + }, + "FilterConjunction": { + "type": "string", + "description": "Filter conjunction mode.", + "enum": [ + "all", + "any" + ] + }, + "ForbiddenErrorResponseContent": { + "type": "object", + "description": "The caller is authenticated but not allowed to access the requested resource.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 403, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ForbiddenErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "ForbiddenErrorType": { + "type": "string", + "description": "Problem type URI for authorization errors.", + "enum": [ + "/errors/insufficient-permissions" + ] + }, + "GetDocumentIngestionResponseContent": { + "type": "object", + "properties": { + "ingestion": { + "$ref": "#/components/schemas/DocumentIngestionRecord" + } + }, + "required": [ + "ingestion" + ] + }, + "GetIngestionHookStatusResponseContent": { + "type": "object", + "properties": { + "status": { + "$ref": "#/components/schemas/IngestionHookStatus" + } + }, + "required": [ + "status" + ] + }, + "GetLongTermMemoryResponseContent": { + "type": "object", + "description": "A long-term memory record.", + "properties": { + "id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Unique memory record ID." + }, + "text": { + "type": "string", + "description": "The memory content." + }, + "memoryType": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Type of memory. Built-in or a custom type registered on the store." + }, + "sessionId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Session ID associated with this memory." + }, + "ownerId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Owner ID associated with this memory." + }, + "namespace": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Deprecated namespace label. Absent when the record uses namespaceRef or has no namespace.", + "deprecated": true + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRef" + }, + "topics": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "description": "Topic name (1-100 chars)." + }, + "maxItems": 50, + "description": "Topic tags for categorization." + }, + "attributes": { + "$ref": "#/components/schemas/AttributeMap" + }, + "createdAt": { + "type": "string", + "description": "Timestamp when the memory was created (UTC).", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Timestamp when the memory was last updated (UTC).", + "format": "date-time" + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemorySourceLink" + }, + "description": "Generic source links for this memory. Document sources include the customer document ID and revision that produced the memory." + }, + "sourcesNextPageToken": { + "type": "string", + "description": "Opaque token for additional source links when this response truncated attribution. Absent when every link is included." + }, + "documentManaged": { + "type": "boolean", + "description": "True when this memory was created by document ingestion and is protected from generic overwrite." + } + }, + "required": [ + "createdAt", + "id", + "text", + "updatedAt" + ] + }, + "GetNamespaceResponseContent": { + "type": "object", + "properties": { + "namespace": { + "$ref": "#/components/schemas/Namespace" + } + }, + "required": [ + "namespace" + ] + }, + "GetNamespaceRoutingResponseContent": { + "type": "object", + "properties": { + "policy": { + "$ref": "#/components/schemas/NamespaceRoutingPolicy" + } + }, + "required": [ + "policy" + ] + }, + "GetSessionEventResponseContent": { + "type": "object", + "description": "Response containing a single requested session event.", + "properties": { + "event": { + "$ref": "#/components/schemas/SessionEvent" + } + }, + "required": [ + "event" + ] + }, + "GetSessionMemoryResponseContent": { + "type": "object", + "description": "Session memory state for a session.", + "properties": { + "sessionId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The session ID." + }, + "ownerId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "The owner of the session, set from the actorId of the first event." }, "namespace": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Optional namespace for the session, used as the namespace of every long-term memory extracted from it.\n\nLike `ownerId`, this is a session-level value taken from the first event: the value sent with the first event of a session is stored on the session, and values sent with later events are ignored. Omit it to leave extracted memories without a namespace." + "description": "Deprecated namespace label for the session, set from the first event. Absent when the session uses namespaceRef or has no namespace.", + "deprecated": true + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRef" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionEvent" + }, + "description": "Conversation events retained for this session. When summarization is enabled, events already covered by the summary are excluded unless `includeSummarisedEvents` is set; the compacted summary is returned separately in `summary`." + }, + "summary": { + "$ref": "#/components/schemas/SessionSummary" + } + }, + "required": [ + "events", + "ownerId", + "sessionId" + ] + }, + "GetSummaryViewResponseContent": { + "type": "object", + "description": "An owned Summary View definition. Results are separate derived state and never enter long-term-memory search.", + "properties": { + "id": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "ownerId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Immutable canonical user ID assigned by the server. Clients cannot set ownership." + }, + "visibility": { + "$ref": "#/components/schemas/SummaryViewVisibility" + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "description": "Customer-visible Summary View name (1-100 characters)." + }, + "description": { + "type": "string", + "maxLength": 200, + "description": "Optional Summary View description (up to 200 characters)." + }, + "source": { + "$ref": "#/components/schemas/SummaryViewSource" + }, + "filter": { + "$ref": "#/components/schemas/LongTermMemoryFilter" + }, + "filterOp": { + "$ref": "#/components/schemas/FilterConjunction" + }, + "groupBy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SummaryViewGroupField" + }, + "maxItems": 4 + }, + "evictionPolicy": { + "$ref": "#/components/schemas/SummaryViewEvictionPolicy" + }, + "timeWindowSeconds": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "output": { + "$ref": "#/components/schemas/SummaryViewOutputConfig" + }, + "refreshPolicy": { + "$ref": "#/components/schemas/SummaryViewRefreshPolicy" + }, + "definitionVersion": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "state": { + "$ref": "#/components/schemas/SummaryViewState" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "definitionVersion", + "filterOp", + "groupBy", + "id", + "name", + "output", + "ownerId", + "refreshPolicy", + "source", + "state", + "updatedAt", + "visibility" + ] + }, + "GetSummaryViewResultResponseContent": { + "type": "object", + "properties": { + "viewId": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "scope": { + "$ref": "#/components/schemas/SummaryViewScope" + }, + "status": { + "$ref": "#/components/schemas/SummaryViewResultStatus" + }, + "stale": { + "type": "boolean" + }, + "format": { + "$ref": "#/components/schemas/SummaryViewOutputFormat" + }, + "value": { + "$ref": "#/components/schemas/SummaryViewResultValue" + }, + "sourceMemoryCount": { + "type": "integer", + "minimum": 0, + "description": "Number of matching source memories. When selection stops early, a separate count query measures the matching total without reading the remaining source pages. Concurrent writes can change the source set between queries; the count is never less than the number of records already observed.", + "format": "int64" + }, + "summarizedMemoryCount": { + "type": "integer", + "minimum": 0, + "description": "Number of matching source memories included in the generated value after bounded deterministic selection.", + "format": "int64" + }, + "truncated": { + "type": "boolean", + "description": "Combined signal for sourcesOmitted or sourceContentTruncated." + }, + "sourcesOmitted": { + "type": "boolean", + "description": "True when matching source memories were excluded from generation, including when further source pages were not read after reaching an input budget." + }, + "sourceContentTruncated": { + "type": "boolean", + "description": "True when content from an included source memory was clipped to fit a source or generation input budget. This can be true even when sourceMemoryCount equals summarizedMemoryCount." + }, + "sourceMemoryIds": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + }, + "description": "The memory IDs selected for the materialized value. Included only when requested with includeSourceMemoryIds=true." + }, + "sourceUpdatedAt": { + "type": "string", + "format": "date-time" + }, + "computedAt": { + "type": "string", + "format": "date-time" + }, + "definitionVersion": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "errorCode": { + "$ref": "#/components/schemas/SummaryViewResultErrorCode" + } + }, + "required": [ + "definitionVersion", + "format", + "scope", + "sourceContentTruncated", + "sourceMemoryCount", + "sourcesOmitted", + "stale", + "status", + "summarizedMemoryCount", + "truncated", + "viewId" + ] + }, + "HookStatusSource": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "IngestionHookRetryErrorResponseContent": { + "type": "object", + "description": "A saved ingestion hook decision is waiting for its next bounded attempt.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 503, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/IngestionHookRetryErrorType" + }, + "retryAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "retryAt", + "status", + "title", + "type" + ] + }, + "IngestionHookRetryErrorType": { + "type": "string", + "enum": [ + "/errors/ingestion-hook-retry" + ] + }, + "IngestionHookStatus": { + "type": "object", + "description": "Compact hook metadata, retained for seven days after completion or expiry. Application state is independent of the decision state; earlier writes may have completed before a later failure.", + "properties": { + "eventId": { + "type": "string" + }, + "ingestionOperationId": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "path": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/HookStatusSource" + }, + "configRevision": { + "type": "integer", + "format": "int64" + }, + "state": { + "type": "string" + }, + "applyState": { + "type": "string" + }, + "attempts": { + "type": "integer", + "format": "int32" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "deadline": { + "type": "string", + "format": "date-time" + }, + "nextAttemptAt": { + "type": "string", + "format": "date-time" + }, + "lastError": { + "type": "string" + }, + "applied": { + "type": "integer", + "format": "int32" + }, + "filtered": { + "type": "integer", + "format": "int32" + }, + "conflicts": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "applied", + "applyState", + "attempts", + "configRevision", + "conflicts", + "createdAt", + "deadline", + "eventId", + "filtered", + "ingestionOperationId", + "ownerId", + "path", + "phase", + "source", + "state", + "updatedAt" + ] + }, + "IngestionHookStoppedErrorResponseContent": { + "type": "object", + "description": "The ingestion hook stopped this operation without applying unapproved content.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 422, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/IngestionHookStoppedErrorType" + }, + "state": { + "$ref": "#/components/schemas/IngestionHookStoppedState" + } + }, + "required": [ + "state", + "status", + "title", + "type" + ] + }, + "IngestionHookStoppedErrorType": { + "type": "string", + "enum": [ + "/errors/ingestion-hook-stopped" + ] + }, + "IngestionHookStoppedState": { + "type": "string", + "enum": [ + "rejected", + "failed", + "expired", + "cancelled", + "conflict" + ] + }, + "ListDocumentIngestionOutputsResponseContent": { + "type": "object", + "properties": { + "memoryIds": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + } + } + }, + "required": [ + "memoryIds" + ] + }, + "ListIngestionHookStatusesResponseContent": { + "type": "object", + "properties": { + "statuses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IngestionHookStatus" + } }, - "role": { - "$ref": "#/components/schemas/MessageRole" + "nextCursor": { + "type": "string" + } + }, + "required": [ + "statuses" + ] + }, + "ListMemorySourceEvidenceResponseContent": { + "type": "object", + "properties": { + "source": { + "$ref": "#/components/schemas/MemorySourceLink" + } + }, + "required": [ + "source" + ] + }, + "ListMemorySourceLinksResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemorySourceLink" + } }, - "content": { + "nextPageToken": { + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "ListNamespacesResponseContent": { + "type": "object", + "properties": { + "items": { "type": "array", "items": { - "$ref": "#/components/schemas/Content" - }, - "description": "The message content as an array of typed content parts." + "$ref": "#/components/schemas/Namespace" + } }, - "createdAt": { + "nextPageToken": { "type": "string", - "description": "Client-supplied timestamp for when the event happened (UTC).", - "format": "date-time" - }, - "metadata": { - "description": "Optional metadata as any valid JSON value." + "description": "Opaque token for fetching the next page. Omitted when no more results." } }, "required": [ - "actorId", - "content", - "createdAt", - "role" + "items" ] }, - "AddSessionEventResponseContent": { + "ListSessionSummariesResponseContent": { "type": "object", - "description": "Response returned after appending a session event.", + "description": "Paginated session rows for a store.", "properties": { - "event": { - "$ref": "#/components/schemas/SessionEvent" + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionListItem" + } + }, + "total": { + "type": "integer", + "format": "int32" + }, + "nextPageToken": { + "type": "string" } }, "required": [ - "event" + "items", + "total" ] }, - "AttributeFilterClause": { - "description": "Filter clause for a single record attribute. Exactly one member is set; the member must match the attribute's declared field type.", - "oneOf": [ - { - "type": "object", - "title": "string", - "properties": { - "string": { - "type": "string", - "description": "Match a string attribute equal to this value." - } + "ListSessionsResponseContent": { + "type": "object", + "description": "Paginated list of session IDs for a store.", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." }, - "required": [ - "string" - ] + "description": "List of session IDs. Always returned; empty when the store has no sessions." }, - { - "type": "object", - "title": "number", - "properties": { - "number": { - "type": "number", - "description": "Match a numeric attribute equal to this value.", - "format": "double" - } - }, - "required": [ - "number" - ] + "total": { + "type": "integer", + "description": "Total number of matching sessions.", + "format": "int32" }, - { - "type": "object", - "title": "boolean", - "properties": { - "boolean": { - "type": "boolean", - "description": "Match a boolean attribute equal to this value." - } - }, - "required": [ - "boolean" - ] + "nextPageToken": { + "type": "string", + "description": "Opaque token for fetching the next page. Omitted when no more results." + } + }, + "required": [ + "items", + "total" + ] + }, + "ListSourceMemoriesResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemoryRecord" + } }, - { - "type": "object", - "title": "list", - "properties": { - "list": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Match a list attribute that contains all of these values." - } - }, - "required": [ - "list" - ] + "nextPageToken": { + "type": "string" } + }, + "required": [ + "items" ] }, - "AttributeFilterMap": { + "ListSummaryViewsResponseContent": { "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/AttributeFilterClause" + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SummaryView" + } + }, + "nextPageToken": { + "type": "string" + } }, - "description": "Per-attribute filter clauses keyed by the custom memory type's field name." + "required": [ + "items" + ] }, - "AttributeMap": { + "LlmPromptSpec": { "type": "object", - "additionalProperties": {}, - "description": "Structured record attributes: a map from the custom memory type's field name to that field's value. Values are arbitrary JSON matching the field's declared type (str, int, float, bool, list[str], list[float], or object)." + "description": "Customer-authored prompt evaluated by the LLM routing strategy.", + "properties": { + "prompt": { + "type": "string", + "maxLength": 2000, + "minLength": 1 + } + }, + "required": [ + "prompt" + ] }, - "AuthenticationErrorResponseContent": { + "LongTermMemoryFilter": { + "type": "object", + "description": "Grouping of filter conditions for long-term memory search.", + "properties": { + "sessionId": { + "$ref": "#/components/schemas/SessionIdFilter" + }, + "ownerId": { + "$ref": "#/components/schemas/OwnerIdFilter" + }, + "namespace": { + "$ref": "#/components/schemas/NamespaceFilter" + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRefFilter" + }, + "unnamespaced": { + "type": "boolean", + "description": "When true, matches only records that have neither a deprecated namespace label nor a namespace resource placement. This scope is always combined with every other filter condition. Do not supply namespace or namespaceRef with this field." + }, + "topics": { + "$ref": "#/components/schemas/TopicsFilter" + }, + "memoryType": { + "$ref": "#/components/schemas/MemoryTypeFilter" + }, + "createdAt": { + "$ref": "#/components/schemas/CreatedAtFilter" + }, + "attributes": { + "$ref": "#/components/schemas/AttributeFilterMap" + } + } + }, + "LongTermMemorySearchSortBy": { + "type": "string", + "description": "Timestamp field used to order long-term memory search results.", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "LongTermMemorySearchSortOrder": { + "type": "string", + "description": "Direction used to order long-term memory search results.", + "enum": [ + "asc", + "desc" + ] + }, + "MatcherKind": { + "type": "string", + "description": "The matching techniques this API supports.", + "enum": [ + "regex" + ] + }, + "MatcherSpec": { + "type": "object", + "description": "How a detector matches, as a tagged union of techniques.\n\nExactly the block naming the declared kind must be present; a block belonging to\nany other kind is rejected rather than guessed at. Further kinds are additive.", + "properties": { + "kind": { + "$ref": "#/components/schemas/MatcherKind" + }, + "regex": { + "$ref": "#/components/schemas/RegexSpec" + } + }, + "required": [ + "kind" + ] + }, + "MemoryFailedDependencyErrorResponseContent": { "type": "object", - "description": "Authentication credentials are missing, malformed, or invalid.", + "description": "A resource or model-provider dependency required by Agent Memory could not complete the request.", "properties": { "title": { "type": "string", @@ -2001,7 +7560,7 @@ }, "status": { "type": "integer", - "default": 401, + "default": 424, "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", "format": "int32" }, @@ -2014,7 +7573,7 @@ "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." }, "type": { - "$ref": "#/components/schemas/AuthenticationErrorType" + "$ref": "#/components/schemas/MemoryFailedDependencyErrorType" } }, "required": [ @@ -2023,292 +7582,660 @@ "type" ] }, - "AuthenticationErrorType": { + "MemoryFailedDependencyErrorType": { "type": "string", - "description": "Problem type URI for authentication errors.", + "description": "Problem type URI for Agent Memory failed-dependency errors.", "enum": [ - "/errors/authentication-failed" + "/errors/resource-unavailable", + "/errors/database-out-of-memory", + "/errors/invalid-model-credentials", + "/errors/model-quota-exceeded" ] }, - "BadRequestErrorResponseContent": { + "MemoryRecord": { "type": "object", - "description": "Request validation or input decoding failed.", + "description": "A long-term memory record.", "properties": { - "title": { + "id": { "type": "string", - "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Unique memory record ID." }, - "status": { + "text": { + "type": "string", + "description": "The memory content." + }, + "memoryType": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Type of memory. Built-in or a custom type registered on the store." + }, + "sessionId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Session ID associated with this memory." + }, + "ownerId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Owner ID associated with this memory." + }, + "namespace": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Deprecated namespace label. Absent when the record uses namespaceRef or has no namespace.", + "deprecated": true + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRef" + }, + "topics": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "description": "Topic name (1-100 chars)." + }, + "maxItems": 50, + "description": "Topic tags for categorization." + }, + "attributes": { + "$ref": "#/components/schemas/AttributeMap" + }, + "createdAt": { + "type": "string", + "description": "Timestamp when the memory was created (UTC).", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Timestamp when the memory was last updated (UTC).", + "format": "date-time" + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemorySourceLink" + }, + "description": "Generic source links for this memory. Document sources include the customer document ID and revision that produced the memory." + }, + "sourcesNextPageToken": { + "type": "string", + "description": "Opaque token for additional source links when this response truncated attribution. Absent when every link is included." + }, + "documentManaged": { + "type": "boolean", + "description": "True when this memory was created by document ingestion and is protected from generic overwrite." + } + }, + "required": [ + "createdAt", + "id", + "text", + "updatedAt" + ] + }, + "MemorySourceEvidence": { + "type": "object", + "properties": { + "evidenceId": { + "type": "string", + "maxLength": 128, + "minLength": 1, + "description": "Identity of a logical extraction occurrence. Delivery retries retain this ID; a new extraction receives a new ID. Absent on legacy evidence." + }, + "schemaVersion": { "type": "integer", - "default": 400, - "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "description": "Version of the evidence metadata contract, currently 1. Absent on legacy evidence.", "format": "int32" }, - "detail": { + "capturedAt": { "type": "string", - "description": "A human-readable explanation specific to this occurrence of the problem." + "description": "UTC time when the producer prepared the input for extraction, before model invocation. Preserved through checkpoint replay. This is not the source event time.", + "format": "date-time" }, - "instance": { + "producer": { + "$ref": "#/components/schemas/EvidenceProducer" + }, + "ingestionId": { "type": "string", - "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated document ingestion identifier." }, - "type": { - "$ref": "#/components/schemas/BadRequestErrorType" + "chunkId": { + "type": "string" }, - "fields": { + "start": { + "type": "integer", + "format": "int32" + }, + "end": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "chunkId", + "ingestionId" + ] + }, + "MemorySourceKind": { + "type": "string", + "enum": [ + "document", + "session" + ] + }, + "MemorySourceLink": { + "type": "object", + "properties": { + "sourceId": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/MemorySourceKind" + }, + "identity": { + "$ref": "#/components/schemas/DocumentSourceIdentity" + }, + "session": { + "$ref": "#/components/schemas/SessionSourceIdentity" + }, + "evidence": { "type": "array", "items": { - "$ref": "#/components/schemas/FieldError" - }, - "description": "Optional field-level validation errors." + "$ref": "#/components/schemas/MemorySourceEvidence" + } + }, + "sessionEvidence": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionSourceEvidence" + } + }, + "evidenceNextPageToken": { + "type": "string", + "description": "Opaque token for GET /v1/stores/{storeId}/long-term-memory/{memoryId}/sources/{sourceId}/evidence. Continues evidence or sessionEvidence for this source; absent when every item is included." } }, "required": [ - "status", - "title", - "type" + "kind", + "sourceId" ] }, - "BadRequestErrorType": { + "MemoryTypeFilter": { + "type": "object", + "description": "Filter by memory type. Matching is case-insensitive.", + "properties": { + "eq": { + "type": "string", + "description": "Equals the given value." + }, + "ne": { + "type": "string", + "description": "Not equal to the given value." + }, + "in": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Matches any of the given values." + }, + "all": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Matches all of the given values." + } + } + }, + "MessageRole": { "type": "string", - "description": "Problem type URI for bad request errors.", + "description": "Message role for session events.", "enum": [ - "/errors/invalid-data" + "USER", + "ASSISTANT", + "SYSTEM" ] }, - "BulkCreateLongTermMemoriesRequestContent": { + "MoveLongTermMemoriesRequestContent": { "type": "object", - "description": "Bulk create request for long-term memory records.", "properties": { - "memories": { + "memoryIds": { "type": "array", "items": { - "$ref": "#/components/schemas/CreateMemoryRecord" + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." }, "maxItems": 100, - "minItems": 1, - "description": "List of memory records to create." + "minItems": 1 + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRefInput" + } + }, + "required": [ + "memoryIds" + ] + }, + "MoveLongTermMemoriesResponseContent": { + "type": "object", + "properties": { + "moved": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + } + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BulkOperationError" + } + } + }, + "required": [ + "moved" + ] + }, + "MutableSummaryViewState": { + "type": "string", + "description": "States a caller may set directly. Deleting is entered only by the delete operation.", + "enum": [ + "active", + "disabled" + ] + }, + "Namespace": { + "type": "object", + "description": "A namespace hierarchy record.", + "properties": { + "namespaceId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated namespace identifier." + }, + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "One case-sensitive namespace path segment." + }, + "parentId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated namespace identifier." + }, + "rootId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated namespace identifier." + }, + "ownerId": { + "type": "string", + "minLength": 1, + "description": "An opaque application-user ID selected by the platform for a namespace root." + }, + "scope": { + "$ref": "#/components/schemas/NamespaceScope" + }, + "state": { + "$ref": "#/components/schemas/NamespaceState" + }, + "revision": { + "type": "integer", + "format": "int64" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "path": { + "type": "string", + "maxLength": 1024, + "minLength": 1, + "description": "Current case-sensitive path, derived from the live hierarchy." } }, "required": [ - "memories" + "createdAt", + "name", + "namespaceId", + "path", + "revision", + "rootId", + "scope", + "state", + "updatedAt" ] }, - "BulkCreateLongTermMemoriesResponseContent": { + "NamespaceFilter": { "type": "object", - "description": "Bulk create result for long-term memory records.", + "description": "Filter by namespace. Matching is case-insensitive.\n\nNamespace is optional, so records without one need care. The positive operators (`eq`, `in`, `all`) require the field to be present and therefore never match a record that has no namespace \u2014 including memories extracted from a session that has none. `ne` is a plain negation and *does* match those records, so use `eq` or `in` when you need strict namespace isolation rather than `ne` against the namespaces you want to exclude.\n\nOmit this filter to search across every namespace, and use it to keep recall inside one project, workspace, team, environment, or tenant.", "properties": { - "created": { + "eq": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Equals the given value.", + "deprecated": true + }, + "ne": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Not equal to the given value.", + "deprecated": true + }, + "in": { "type": "array", "items": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + "description": "Deprecated flat namespace label for logical grouping. The value is 1-64 alphanumeric characters or dashes and has no hierarchy or scope semantics. Matching through `filter.namespace` is case-insensitive.\n\nThe field remains optional for backward compatibility. New integrations should use `namespaceRef` to reference an existing namespace resource.\n\nRecords produced by extraction inherit the namespace label stored on the session when the session was created.\n\nThis shape is deprecated: Use namespaceRef to reference a namespace resource.", + "deprecated": true }, - "description": "IDs of successfully created memory records. Always returned; empty when no records were created." + "description": "Matches any of the given values." }, - "errors": { + "all": { "type": "array", "items": { - "$ref": "#/components/schemas/BulkOperationError" + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Deprecated flat namespace label for logical grouping. The value is 1-64 alphanumeric characters or dashes and has no hierarchy or scope semantics. Matching through `filter.namespace` is case-insensitive.\n\nThe field remains optional for backward compatibility. New integrations should use `namespaceRef` to reference an existing namespace resource.\n\nRecords produced by extraction inherit the namespace label stored on the session when the session was created.\n\nThis shape is deprecated: Use namespaceRef to reference a namespace resource.", + "deprecated": true }, - "description": "Errors for memory records that failed to be created." + "description": "Matches all of the given values." + } + } + }, + "NamespaceRef": { + "type": "object", + "description": "Reference to a namespace resource with its stable identifier and current display data.", + "properties": { + "namespaceId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Stable namespace resource identifier." + }, + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Current namespace name." + }, + "path": { + "type": "string", + "maxLength": 1024, + "minLength": 1, + "description": "Current namespace path." } }, "required": [ - "created" + "name", + "namespaceId", + "path" ] }, - "BulkDeleteLongTermMemoriesRequestContent": { + "NamespaceRefFilter": { "type": "object", - "description": "Bulk delete request for long-term memory records.", + "description": "Filter by stable namespace resource identifier. Supports exact or any-of matching and matches only resource-backed records, never deprecated namespace labels.", "properties": { - "memoryIds": { + "eq": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Equals the given value." + }, + "in": { "type": "array", "items": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." + "description": "Server-generated namespace identifier." }, - "maxItems": 100, - "minItems": 1, - "description": "List of memory record IDs to delete (1-100 IDs)." + "description": "Matches any of the given values." + } + } + }, + "NamespaceRefInput": { + "type": "object", + "description": "Reference to an existing namespace resource for a write or namespace-scoped operation.", + "properties": { + "namespaceId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Stable namespace resource identifier." } }, "required": [ - "memoryIds" + "namespaceId" ] }, - "BulkDeleteLongTermMemoriesResponseContent": { + "NamespaceRoutingPolicy": { "type": "object", - "description": "Bulk delete result for long-term memory records.", + "description": "Store-level automatic namespace routing and admission policy. Global rules target shared namespaces only. Accept rules select candidates in list order. All enabled reject rules for a destination apply regardless of their position or how the destination was selected.", "properties": { - "deleted": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Memory record identifier (1-64 chars, alphanumeric and dashes). Client-provided for idempotent long-term memory creation." - }, - "description": "IDs of successfully deleted memory records. Always returned; empty when no records were deleted." + "enabled": { + "type": "boolean" }, - "errors": { + "rules": { "type": "array", "items": { - "$ref": "#/components/schemas/BulkOperationError" + "$ref": "#/components/schemas/NamespaceRoutingRule" }, - "description": "Errors for memory records that failed to be deleted." + "maxItems": 32 } }, "required": [ - "deleted" + "enabled" ] }, - "BulkOperationError": { + "NamespaceRoutingRule": { "type": "object", - "description": "Error detail for a single item in a bulk operation.", + "description": "One routing or admission rule. action is accept or reject. Strategies run in list order and the first match fires the rule. An accept with no strategies always matches; a reject with no strategies never matches. A matching accept selects a candidate. Every enabled reject for that destination must pass before placement. A veto skips that candidate during automatic routing and fails explicit placement.", "properties": { "id": { "type": "string", "maxLength": 64, "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Memory record ID of the item that failed." + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Routing rule identifier (1-64 chars). Published in logs, so it is an identifier rather than free text." }, - "error": { - "type": "string", - "description": "Error message describing the failure." + "destination": { + "$ref": "#/components/schemas/NamespaceRefInput" + }, + "enabled": { + "type": "boolean" + }, + "action": { + "$ref": "#/components/schemas/RoutingRuleAction" + }, + "strategies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteCondition" + }, + "maxItems": 8 } }, "required": [ - "error", + "action", + "destination", + "enabled", "id" ] }, - "Content": { - "description": "A single content item in a message. Exactly one member is set.", - "oneOf": [ - { - "type": "object", - "title": "text", - "properties": { - "text": { - "type": "string", - "description": "Text content." - } - }, - "required": [ - "text" - ] - } + "NamespaceScope": { + "type": "string", + "description": "The root scope of a namespace hierarchy.", + "enum": [ + "PERSONAL", + "SHARED" ] }, - "CreateMemoryRecord": { + "NamespaceSortBy": { + "type": "string", + "enum": [ + "id", + "name", + "createdAt", + "updatedAt" + ] + }, + "NamespaceSortOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "NamespaceState": { + "type": "string", + "description": "Whether a namespace accepts new children and placements.", + "enum": [ + "ACTIVE", + "ARCHIVED" + ] + }, + "NamespaceUpdateState": { + "type": "string", + "description": "The archive-only state transition accepted by UpdateNamespace.", + "enum": [ + "ARCHIVED" + ] + }, + "NotFoundErrorResponseContent": { "type": "object", - "description": "A memory record to create.", + "description": "The requested resource does not exist.", "properties": { - "id": { + "title": { "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Client-provided memory record ID for idempotent creation." + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." }, - "text": { - "type": "string", - "maxLength": 50000, - "minLength": 1, - "description": "The memory content (1-50000 chars)." + "status": { + "type": "integer", + "default": 404, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" }, - "memoryType": { + "detail": { "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", - "description": "Type of memory. Built-in or a custom type registered on the store." + "description": "A human-readable explanation specific to this occurrence of the problem." }, - "sessionId": { + "instance": { "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Session ID associated with this memory (1-64 chars, alphanumeric and dashes)." + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." }, - "ownerId": { + "type": { + "$ref": "#/components/schemas/NotFoundErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "NotFoundErrorType": { + "type": "string", + "description": "Problem type URI for not-found errors.", + "enum": [ + "/errors/resource-not-found" + ] + }, + "OwnerIdFilter": { + "type": "object", + "description": "Filter by the long-term memory ownerId field. Matching is case-sensitive.", + "properties": { + "eq": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Owner ID associated with this memory (1-64 chars, alphanumeric and dashes)." + "description": "Equals the given value." }, - "namespace": { + "ne": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Optional namespace for logical grouping (1-64 chars, alphanumeric and dashes). Omit to create the record without a namespace. Records created here are explicit writes, so this value is taken as given; extraction instead inherits the source session's namespace." + "description": "Not equal to the given value." }, - "topics": { + "in": { "type": "array", "items": { - "type": "string", - "maxLength": 100, - "minLength": 1, - "description": "Topic name (1-100 chars)." - }, - "maxItems": 50, - "description": "Topic tags for categorization (max 50 topics, each 1-100 chars)." - }, - "attributes": { - "$ref": "#/components/schemas/AttributeMap" - } - }, - "required": [ - "id", - "ownerId", - "text" - ] - }, - "CreatedAtFilter": { - "type": "object", - "description": "Filter by creation timestamp.", - "properties": { - "gt": { - "type": "string", - "description": "After the given timestamp.", - "format": "date-time" - }, - "gte": { - "type": "string", - "description": "At or after the given timestamp.", - "format": "date-time" - }, - "lt": { - "type": "string", - "description": "Before the given timestamp.", - "format": "date-time" - }, - "lte": { - "type": "string", - "description": "At or before the given timestamp.", - "format": "date-time" + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants." + }, + "description": "Matches any of the given values." }, - "eq": { - "type": "string", - "description": "Exactly at the given timestamp.", - "format": "date-time" + "all": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants." + }, + "description": "Matches all of the given values." } } }, - "FailedDependencyErrorResponseContent": { + "PayloadTooLargeErrorResponseContent": { "type": "object", - "description": "A dependent resource required to process the request is unavailable or unhealthy.", + "description": "The request payload exceeds the maximum supported size.", "properties": { "title": { "type": "string", @@ -2316,7 +8243,7 @@ }, "status": { "type": "integer", - "default": 424, + "default": 413, "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", "format": "int32" }, @@ -2329,7 +8256,7 @@ "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." }, "type": { - "$ref": "#/components/schemas/FailedDependencyErrorType" + "$ref": "#/components/schemas/PayloadTooLargeErrorType" } }, "required": [ @@ -2338,48 +8265,90 @@ "type" ] }, - "FailedDependencyErrorType": { + "PayloadTooLargeErrorType": { "type": "string", - "description": "Problem type URI for failed-dependency errors.", + "description": "Problem type URI for payload-too-large errors.", "enum": [ - "/errors/resource-unavailable", - "/errors/database-out-of-memory" + "/errors/payload-too-large" ] }, - "FieldError": { + "PutNamespaceRoutingRequestContent": { "type": "object", - "description": "Validation error details for a single request field.", "properties": { - "field": { - "type": "string", - "description": "Name of the invalid request field." + "policy": { + "$ref": "#/components/schemas/NamespaceRoutingPolicy" + } + }, + "required": [ + "policy" + ] + }, + "PutNamespaceRoutingResponseContent": { + "type": "object", + "properties": { + "policy": { + "$ref": "#/components/schemas/NamespaceRoutingPolicy" + } + }, + "required": [ + "policy" + ] + }, + "RefreshSummaryViewRequestContent": { + "type": "object", + "properties": { + "target": { + "$ref": "#/components/schemas/SummaryViewRefreshTarget" }, - "rule": { + "scope": { + "$ref": "#/components/schemas/SummaryViewScope" + } + }, + "required": [ + "target" + ] + }, + "RefreshSummaryViewResponseContent": { + "type": "object", + "properties": { + "viewId": { "type": "string", - "description": "Validation rule that was violated." + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." }, - "message": { - "type": "string", - "description": "Human-readable validation error message." + "target": { + "$ref": "#/components/schemas/SummaryViewRefreshTarget" + }, + "accepted": { + "type": "boolean" } }, "required": [ - "field", - "message", - "rule" + "accepted", + "target", + "viewId" ] }, - "FilterConjunction": { - "type": "string", - "description": "Filter conjunction mode.", - "enum": [ - "all", - "any" + "RegexSpec": { + "type": "object", + "description": "A regular expression to match against.\n\nEvaluated by RE2, which runs in linear time \u2014 so an expensive pattern is not a\ndenial-of-service risk, but lookaround and backreferences are unavailable.\nRejected on write if it does not compile, or if it can match without consuming\ntext: such a pattern matches at every position, so it would exclude every memory\nthe store writes.", + "properties": { + "pattern": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "The pattern, in RE2 syntax." + } + }, + "required": [ + "pattern" ] }, - "ForbiddenErrorResponseContent": { + "ResourceSuspendedErrorResponseContent": { "type": "object", - "description": "The caller is authenticated but not allowed to access the requested resource.", + "description": "The requested resource exists but is suspended by an administrator.", "properties": { "title": { "type": "string", @@ -2387,7 +8356,7 @@ }, "status": { "type": "integer", - "default": 403, + "default": 423, "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", "format": "int32" }, @@ -2400,7 +8369,7 @@ "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." }, "type": { - "$ref": "#/components/schemas/ForbiddenErrorType" + "$ref": "#/components/schemas/ResourceSuspendedErrorType" } }, "required": [ @@ -2409,375 +8378,508 @@ "type" ] }, - "ForbiddenErrorType": { + "ResourceSuspendedErrorType": { "type": "string", - "description": "Problem type URI for authorization errors.", + "description": "Problem type URI for suspended resources.", "enum": [ - "/errors/insufficient-permissions" + "/errors/resource-suspended" ] }, - "GetLongTermMemoryResponseContent": { + "RouteCondition": { "type": "object", - "description": "A long-term memory record.", + "description": "One plug-in strategy invocation. Exactly the block naming the declared kind must be present. List order is evaluation order.", "properties": { - "id": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Unique memory record ID." + "kind": { + "$ref": "#/components/schemas/RouteStrategyKind" }, + "llmPrompt": { + "$ref": "#/components/schemas/LlmPromptSpec" + }, + "matcher": { + "$ref": "#/components/schemas/MatcherSpec" + } + }, + "required": [ + "kind" + ] + }, + "RouteStrategyKind": { + "type": "string", + "description": "Routing strategy kinds. Unknown kinds are rejected. New kinds register on the evaluator without changing namespace records.", + "enum": [ + "llm_prompt", + "matcher_spec" + ] + }, + "RoutingRuleAction": { + "type": "string", + "description": "Whether the rule accepts into the destination or rejects that destination.", + "enum": [ + "accept", + "reject" + ] + }, + "SearchLongTermMemoryRequestContent": { + "type": "object", + "description": "Semantic search request for long-term memory records.", + "properties": { "text": { "type": "string", - "description": "The memory content." + "description": "Text to use for semantic search." }, - "memoryType": { + "similarityThreshold": { + "type": "number", + "maximum": 1, + "minimum": 0, + "description": "The minimum similarity threshold for vector search results (normalized cosine similarity). Allowed range: 0-1.", + "format": "float" + }, + "filter": { + "$ref": "#/components/schemas/LongTermMemoryFilter" + }, + "excludeDocumentManaged": { + "type": "boolean", + "description": "When true, omit document-managed memories from results. Automatic session reconciliation uses this; public search omits it so document memories remain visible." + }, + "filterOp": { + "$ref": "#/components/schemas/FilterConjunction" + }, + "sortBy": { + "$ref": "#/components/schemas/LongTermMemorySearchSortBy" + }, + "sortOrder": { + "$ref": "#/components/schemas/LongTermMemorySearchSortOrder" + }, + "limit": { + "type": "integer", + "default": 10, + "maximum": 100, + "minimum": 1, + "description": "Maximum number of results to return. Defaults to 10. Allowed range: 1-100.", + "format": "int32" + }, + "pageToken": { "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", - "description": "Type of memory. Built-in or a custom type registered on the store." + "description": "Opaque token from a previous response for the next page." + } + } + }, + "SearchLongTermMemoryResponseContent": { + "type": "object", + "description": "Semantic search result for long-term memory records.", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemoryRecord" + }, + "description": "List of matching memory records. Always returned; empty when no records match." }, - "sessionId": { + "nextPageToken": { "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Session ID associated with this memory." + "description": "Opaque token for fetching the next page. Omitted when no more results." + } + }, + "required": [ + "items" + ] + }, + "SessionEvent": { + "type": "object", + "description": "A single event in the session history.", + "properties": { + "eventId": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Auto-generated event ID." }, - "ownerId": { + "actorId": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Owner ID associated with this memory." + "description": "Unique actor identifier (1-255 chars). Can represent a user, agent, or any participant." }, - "namespace": { + "sessionId": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Namespace for logical grouping. Absent when the record has none: either it was written directly without one, or it was extracted from a session that has no namespace." + "description": "Session identifier (client-provided or server-generated)." }, - "topics": { + "role": { + "$ref": "#/components/schemas/MessageRole" + }, + "content": { "type": "array", "items": { - "type": "string", - "maxLength": 100, - "minLength": 1, - "description": "Topic name (1-100 chars)." + "$ref": "#/components/schemas/Content" }, - "maxItems": 50, - "description": "Topic tags for categorization." - }, - "attributes": { - "$ref": "#/components/schemas/AttributeMap" + "description": "The message content as an array of typed content parts." }, "createdAt": { "type": "string", - "description": "Timestamp when the memory was created (UTC).", + "description": "Client-supplied timestamp for when the event happened (UTC).", "format": "date-time" }, - "updatedAt": { + "systemTimestamp": { "type": "string", - "description": "Timestamp when the memory was last updated (UTC).", + "description": "Server-set timestamp for when the dataplane ingested the event (UTC).", "format": "date-time" + }, + "metadata": { + "description": "Optional metadata as any valid JSON value." } }, "required": [ + "actorId", + "content", "createdAt", - "id", - "text", - "updatedAt" + "eventId", + "role", + "sessionId", + "systemTimestamp" ] }, - "GetSessionEventResponseContent": { + "SessionIdFilter": { "type": "object", - "description": "Response containing a single requested session event.", + "description": "Filter by the long-term memory sessionId field. Matching is case-sensitive.", "properties": { - "event": { - "$ref": "#/components/schemas/SessionEvent" + "eq": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Equals the given value." + }, + "ne": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Not equal to the given value." + }, + "in": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." + }, + "description": "Matches any of the given values." + }, + "all": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." + }, + "description": "Matches all of the given values." } - }, - "required": [ - "event" - ] + } }, - "GetSessionMemoryResponseContent": { + "SessionListItem": { "type": "object", - "description": "Session memory state for a session.", + "description": "One session row without events or summary text. Owner ID and timestamps are absent when unknown on legacy sessions.", "properties": { "sessionId": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The session ID." + "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." }, "ownerId": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The owner of the session, set from the actorId of the first event." + "description": "The owner of the session, set from the actorId of the first event. Absent for a legacy session that has no recorded owner." }, - "namespace": { + "createdAt": { "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "The namespace of the session, set from the namespace of the first event. Absent when the first event did not supply one; long-term memories extracted from this session then carry no namespace." + "format": "date-time" }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionEvent" - }, - "description": "Conversation events retained for this session. When summarization is enabled, events already covered by the summary are excluded unless `includeSummarisedEvents` is set; the compacted summary is returned separately in `summary`." + "updatedAt": { + "type": "string", + "format": "date-time" }, - "summary": { - "$ref": "#/components/schemas/SessionSummary" + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRef" } }, "required": [ - "events", - "ownerId", "sessionId" ] }, - "HealthResponseContent": { - "type": "object", - "description": "Health check response.", - "properties": { - "status": { - "$ref": "#/components/schemas/HealthStatus" - } - }, - "required": [ - "status" + "SessionListSortBy": { + "type": "string", + "enum": [ + "sessionId", + "createdAt", + "updatedAt" ] }, - "HealthStatus": { + "SessionListSortOrder": { "type": "string", - "description": "Health status returned by the /health endpoint.", "enum": [ - "healthy" + "asc", + "desc" ] }, - "ListSessionsResponseContent": { + "SessionSourceEvidence": { "type": "object", - "description": "Paginated list of session IDs for a store.", "properties": { - "items": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." - }, - "description": "List of session IDs. Always returned; empty when the store has no sessions." + "evidenceId": { + "type": "string", + "maxLength": 128, + "minLength": 1, + "description": "Identity of a logical extraction occurrence. Delivery retries retain this ID; a new extraction receives a new ID. Absent on legacy evidence." }, - "total": { + "schemaVersion": { "type": "integer", - "description": "Total number of matching sessions.", + "description": "Version of the evidence metadata contract, currently 1. Absent on legacy evidence.", "format": "int32" }, - "nextPageToken": { + "capturedAt": { "type": "string", - "description": "Opaque token for fetching the next page. Omitted when no more results." + "description": "UTC time when the producer prepared the input for extraction, before model invocation. Preserved through checkpoint replay. This is not the source event time.", + "format": "date-time" + }, + "producer": { + "$ref": "#/components/schemas/EvidenceProducer" + }, + "eventIds": { + "type": "array", + "items": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated event identifier (exactly 32 alphanumeric characters, UUID without dashes)." + }, + "description": "Ordered IDs of events whose content was supplied to one model request. Input attribution, not fact citations." } }, "required": [ - "items", - "total" + "eventIds" ] }, - "LongTermMemoryFilter": { - "type": "object", - "description": "Grouping of filter conditions for long-term memory search.", - "properties": { - "sessionId": { - "$ref": "#/components/schemas/SessionIdFilter" - }, - "ownerId": { - "$ref": "#/components/schemas/OwnerIdFilter" - }, - "namespace": { - "$ref": "#/components/schemas/NamespaceFilter" - }, - "topics": { - "$ref": "#/components/schemas/TopicsFilter" - }, - "memoryType": { - "$ref": "#/components/schemas/MemoryTypeFilter" - }, - "createdAt": { - "$ref": "#/components/schemas/CreatedAtFilter" - }, - "attributes": { - "$ref": "#/components/schemas/AttributeFilterMap" - } - } - }, - "MemoryRecord": { + "SessionSourceIdentity": { "type": "object", - "description": "A long-term memory record.", "properties": { - "id": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Unique memory record ID." - }, - "text": { - "type": "string", - "description": "The memory content." - }, - "memoryType": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", - "description": "Type of memory. Built-in or a custom type registered on the store." - }, "sessionId": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Session ID associated with this memory." + "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." }, "ownerId": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Owner ID associated with this memory." - }, - "namespace": { + "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants." + } + }, + "required": [ + "ownerId", + "sessionId" + ] + }, + "SessionSummary": { + "type": "object", + "description": "Compacted summary of older session history for a session.\n\nThis is derived session state maintained by the background summarization workflow, not a conversation event. It is present only after at least one summarization cycle has completed.", + "properties": { + "text": { "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Namespace for logical grouping. Absent when the record has none: either it was written directly without one, or it was extracted from a session that has no namespace." - }, - "topics": { - "type": "array", - "items": { - "type": "string", - "maxLength": 100, - "minLength": 1, - "description": "Topic name (1-100 chars)." - }, - "maxItems": 50, - "description": "Topic tags for categorization." + "description": "The summary text, a single coherent block." }, - "attributes": { - "$ref": "#/components/schemas/AttributeMap" + "summarizedUpToEventId": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Boundary event ID. This event and all events before it (by array position) are covered by the summary; `events` contains the retained turns after this boundary." }, "createdAt": { "type": "string", - "description": "Timestamp when the memory was created (UTC).", + "description": "Timestamp when the summary was first created (UTC). Preserved across re-summarization cycles.", "format": "date-time" }, "updatedAt": { "type": "string", - "description": "Timestamp when the memory was last updated (UTC).", + "description": "Timestamp when the summary was last regenerated (UTC). Updated on every summarization cycle. Equal to createdAt on the first cycle.", "format": "date-time" + }, + "metadata": { + "description": "Metadata as a JSON object (e.g., model name, token count). Always present; defaults to an empty object when no metadata was supplied." + }, + "summarizedEvents": { + "type": "integer", + "minimum": 1, + "description": "Cumulative number of session events folded into this summary across all summarization cycles. Always positive. A value of 1 may indicate a summary created before this field was tracked; it self-corrects to a real count on the next summarization cycle.", + "format": "int64" } }, "required": [ "createdAt", - "id", + "metadata", + "summarizedEvents", + "summarizedUpToEventId", "text", "updatedAt" ] }, - "MemoryTypeFilter": { + "StoreHealthFeature": { "type": "object", - "description": "Filter by memory type. Matching is case-insensitive.", + "description": "Feature-level store health.", "properties": { - "eq": { - "type": "string", - "description": "Equals the given value." - }, - "ne": { - "type": "string", - "description": "Not equal to the given value." - }, - "in": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Matches any of the given values." - }, - "all": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Matches all of the given values." + "status": { + "$ref": "#/components/schemas/StoreHealthStatus" } + }, + "required": [ + "status" + ] + }, + "StoreHealthFeatureMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/StoreHealthFeature" } }, - "MessageRole": { + "StoreHealthResponseContent": { + "type": "object", + "description": "Store-scoped RAM feature health.", + "properties": { + "status": { + "$ref": "#/components/schemas/StoreHealthStatus" + }, + "features": { + "$ref": "#/components/schemas/StoreHealthFeatureMap" + } + }, + "required": [ + "features", + "status" + ] + }, + "StoreHealthStatus": { "type": "string", - "description": "Message role for session events.", + "description": "Store health status.", "enum": [ - "USER", - "ASSISTANT", - "SYSTEM" + "healthy", + "degraded", + "unavailable" ] }, - "NamespaceFilter": { + "SummaryView": { "type": "object", - "description": "Filter by namespace. Matching is case-insensitive.\n\nNamespace is optional, so records without one need care. The positive operators (`eq`, `in`, `all`) require the field to be present and therefore never match a record that has no namespace — including memories extracted from a session that has none. `ne` is a plain negation and *does* match those records, so use `eq` or `in` when you need strict namespace isolation rather than `ne` against the namespaces you want to exclude.\n\nOmit this filter to search across every namespace, and use it to keep recall inside one project, workspace, team, environment, or tenant.", + "description": "An owned Summary View definition. Results are separate derived state and never enter long-term-memory search.", "properties": { - "eq": { + "id": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "ownerId": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Equals the given value." + "description": "Immutable canonical user ID assigned by the server. Clients cannot set ownership." }, - "ne": { + "visibility": { + "$ref": "#/components/schemas/SummaryViewVisibility" + }, + "name": { "type": "string", - "maxLength": 64, + "maxLength": 100, "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Not equal to the given value." + "description": "Customer-visible Summary View name (1-100 characters)." }, - "in": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Namespace for logical grouping (1-64 chars, alphanumeric and dashes).\n\nThis is the shared namespace format for both session memory and long-term memory: sessions carry it as session-level state, and long-term memory records carry it per record. The same constraints apply everywhere it is used.\n\nNamespace is always optional; a record without one is valid. Applications use it to isolate memories by project, workspace, team, environment, or tenant. See `NamespaceFilter` for how records without a namespace behave under search.\n\nA namespace reaches a record one of two ways. Records written directly carry whatever namespace the caller sets. Records produced by extraction inherit the namespace of the session they were extracted from, which is fixed when that session is created; a session with no namespace yields records with no namespace. Existing records are never backfilled when a namespace is introduced later." - }, - "description": "Matches any of the given values." + "description": { + "type": "string", + "maxLength": 200, + "description": "Optional Summary View description (up to 200 characters)." }, - "all": { + "source": { + "$ref": "#/components/schemas/SummaryViewSource" + }, + "filter": { + "$ref": "#/components/schemas/LongTermMemoryFilter" + }, + "filterOp": { + "$ref": "#/components/schemas/FilterConjunction" + }, + "groupBy": { "type": "array", "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Namespace for logical grouping (1-64 chars, alphanumeric and dashes).\n\nThis is the shared namespace format for both session memory and long-term memory: sessions carry it as session-level state, and long-term memory records carry it per record. The same constraints apply everywhere it is used.\n\nNamespace is always optional; a record without one is valid. Applications use it to isolate memories by project, workspace, team, environment, or tenant. See `NamespaceFilter` for how records without a namespace behave under search.\n\nA namespace reaches a record one of two ways. Records written directly carry whatever namespace the caller sets. Records produced by extraction inherit the namespace of the session they were extracted from, which is fixed when that session is created; a session with no namespace yields records with no namespace. Existing records are never backfilled when a namespace is introduced later." + "$ref": "#/components/schemas/SummaryViewGroupField" }, - "description": "Matches all of the given values." + "maxItems": 4 + }, + "evictionPolicy": { + "$ref": "#/components/schemas/SummaryViewEvictionPolicy" + }, + "timeWindowSeconds": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "output": { + "$ref": "#/components/schemas/SummaryViewOutputConfig" + }, + "refreshPolicy": { + "$ref": "#/components/schemas/SummaryViewRefreshPolicy" + }, + "definitionVersion": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "state": { + "$ref": "#/components/schemas/SummaryViewState" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" } - } + }, + "required": [ + "createdAt", + "definitionVersion", + "filterOp", + "groupBy", + "id", + "name", + "output", + "ownerId", + "refreshPolicy", + "source", + "state", + "updatedAt", + "visibility" + ] }, - "NotFoundErrorResponseContent": { + "SummaryViewConflictErrorResponseContent": { "type": "object", - "description": "The requested resource does not exist.", + "description": "A Summary View operation conflicts with the current definition or partition state.", "properties": { "title": { "type": "string", @@ -2785,7 +8887,7 @@ }, "status": { "type": "integer", - "default": 404, + "default": 409, "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", "format": "int32" }, @@ -2798,7 +8900,7 @@ "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." }, "type": { - "$ref": "#/components/schemas/NotFoundErrorType" + "$ref": "#/components/schemas/SummaryViewConflictErrorType" } }, "required": [ @@ -2807,58 +8909,117 @@ "type" ] }, - "NotFoundErrorType": { + "SummaryViewConflictErrorType": { "type": "string", - "description": "Problem type URI for not-found errors.", + "description": "Problem type URI for Summary View conflicts.", "enum": [ - "/errors/resource-not-found" + "/errors/summary-view-conflict" ] }, - "OwnerIdFilter": { + "SummaryViewEvictionPolicy": { "type": "object", - "description": "Filter by the long-term memory ownerId field. Matching is case-sensitive.", + "description": "Selects the bounded set of grouped results. Matching long-term-memory mutations make a scope recent.", "properties": { - "eq": { + "maxPartitions": { + "type": "integer", + "minimum": 1, + "format": "int32" + } + }, + "required": [ + "maxPartitions" + ] + }, + "SummaryViewField": { + "type": "object", + "description": "A field in a bounded structured Summary View result. All fields are optional in generated results.", + "properties": { + "name": { "type": "string", "maxLength": 64, "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Equals the given value." + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$" }, - "ne": { + "description": { "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Not equal to the given value." + "maxLength": 200, + "minLength": 1 }, - "in": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants." - }, - "description": "Matches any of the given values." + "type": { + "$ref": "#/components/schemas/SummaryViewFieldType" + } + }, + "required": [ + "description", + "name", + "type" + ] + }, + "SummaryViewFieldType": { + "type": "string", + "description": "Supported structured-output field types. This matches the custom-memory field vocabulary.", + "enum": [ + "str", + "int", + "float", + "bool", + "list[str]", + "list[float]", + "object" + ] + }, + "SummaryViewGroupField": { + "type": "string", + "description": "A deterministic source field used to partition a Summary View.", + "enum": [ + "ownerId", + "sessionId", + "namespaceRef", + "memoryType" + ] + }, + "SummaryViewOutputConfig": { + "type": "object", + "description": "Text or structured generation configuration. Schema is required exactly when format is structured.", + "properties": { + "format": { + "$ref": "#/components/schemas/SummaryViewOutputFormat" }, - "all": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants." - }, - "description": "Matches all of the given values." + "instructions": { + "type": "string", + "maxLength": 10000, + "description": "Customer-authored generation instructions. Prompt-security validation is enforced by the service." + }, + "schema": { + "$ref": "#/components/schemas/SummaryViewSchema" } - } + }, + "required": [ + "format" + ] }, - "PayloadTooLargeErrorResponseContent": { + "SummaryViewOutputFormat": { + "type": "string", + "description": "Summary View output format.", + "enum": [ + "text", + "structured" + ] + }, + "SummaryViewQuotaCode": { + "type": "string", + "description": "Stable machine-readable Summary View quota code.", + "enum": [ + "DEFINITION_LIMIT_EXCEEDED", + "PARTITION_LIMIT_EXCEEDED", + "SOURCE_LIMIT_EXCEEDED", + "SCHEDULED_GENERATION_LIMIT_EXCEEDED", + "CONCURRENCY_LIMIT_EXCEEDED" + ] + }, + "SummaryViewQuotaErrorResponseContent": { "type": "object", - "description": "The request payload exceeds the maximum supported size.", + "description": "A Summary View operation exceeded an enabled store quota. Exact ceilings are deployment configuration established from benchmark evidence.", "properties": { "title": { "type": "string", @@ -2866,7 +9027,7 @@ }, "status": { "type": "integer", - "default": 413, + "default": 429, "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", "format": "int32" }, @@ -2879,263 +9040,187 @@ "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." }, "type": { - "$ref": "#/components/schemas/PayloadTooLargeErrorType" + "$ref": "#/components/schemas/SummaryViewQuotaErrorType" + }, + "code": { + "$ref": "#/components/schemas/SummaryViewQuotaCode" } }, "required": [ + "code", "status", "title", "type" ] }, - "PayloadTooLargeErrorType": { + "SummaryViewQuotaErrorType": { "type": "string", - "description": "Problem type URI for payload-too-large errors.", + "description": "Problem type URI for Summary View quota errors.", "enum": [ - "/errors/payload-too-large" + "/errors/summary-view-quota-exceeded" ] }, - "SearchLongTermMemoryRequestContent": { + "SummaryViewRefreshMode": { + "type": "string", + "description": "Summary View refresh mode.", + "enum": [ + "manual", + "continuous" + ] + }, + "SummaryViewRefreshPolicy": { "type": "object", - "description": "Semantic search request for long-term memory records.", + "description": "Refresh policy. Continuous views require reconcileIntervalSeconds; manual views omit both interval fields.", "properties": { - "text": { - "type": "string", - "description": "Text to use for semantic search." - }, - "similarityThreshold": { - "type": "number", - "maximum": 1, - "minimum": 0, - "description": "The minimum similarity threshold for vector search results (normalized cosine similarity). Allowed range: 0-1.", - "format": "float" - }, - "filter": { - "$ref": "#/components/schemas/LongTermMemoryFilter" - }, - "filterOp": { - "$ref": "#/components/schemas/FilterConjunction" + "mode": { + "$ref": "#/components/schemas/SummaryViewRefreshMode" }, - "limit": { + "debounceSeconds": { "type": "integer", - "default": 10, - "maximum": 100, "minimum": 1, - "description": "Maximum number of results to return. Defaults to 10. Allowed range: 1-100.", "format": "int32" }, - "pageToken": { - "type": "string", - "description": "Opaque token from a previous response for the next page." + "reconcileIntervalSeconds": { + "type": "integer", + "minimum": 1, + "format": "int32" } - } + }, + "required": [ + "mode" + ] }, - "SearchLongTermMemoryResponseContent": { - "type": "object", - "description": "Semantic search result for long-term memory records.", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MemoryRecord" + "SummaryViewRefreshTarget": { + "type": "string", + "description": "Manual refresh target. Partition refreshes require an exact configured scope; full refreshes reconcile every current and prior partition.", + "enum": [ + "partition", + "full" + ] + }, + "SummaryViewResultErrorCode": { + "type": "string", + "description": "Customer-safe refresh failure code. Provider details and source content are never returned.", + "enum": [ + "SOURCE_LIMIT_EXCEEDED", + "GENERATION_FAILED", + "OUTPUT_VALIDATION_FAILED", + "REFRESH_RETRIES_EXHAUSTED", + "INTERNAL_ERROR" + ] + }, + "SummaryViewResultStatus": { + "type": "string", + "description": "Materialization refresh state.", + "enum": [ + "cold", + "pending", + "ready", + "empty", + "refreshing", + "failed" + ] + }, + "SummaryViewResultValue": { + "description": "Exactly one generated result value. It is omitted for cold, pending, and empty results, and may be omitted for failed results with no last good value.", + "oneOf": [ + { + "type": "object", + "title": "text", + "properties": { + "text": { + "type": "string" + } }, - "description": "List of matching memory records. Always returned; empty when no records match." + "required": [ + "text" + ] }, - "nextPageToken": { - "type": "string", - "description": "Opaque token for fetching the next page. Omitted when no more results." + { + "type": "object", + "title": "data", + "properties": { + "data": {} + }, + "required": [ + "data" + ] } - }, - "required": [ - "items" ] }, - "SessionEvent": { + "SummaryViewSchema": { "type": "object", - "description": "A single event in the session history.", + "description": "Bounded structured-output schema. Nested object schemas, required fields, and arbitrary JSON Schema are not supported in the MVP.", "properties": { - "eventId": { - "type": "string", - "maxLength": 32, - "minLength": 32, - "pattern": "^[a-zA-Z0-9]+$", - "description": "Auto-generated event ID." - }, - "actorId": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Unique actor identifier (1-255 chars). Can represent a user, agent, or any participant." - }, - "sessionId": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Session identifier (client-provided or server-generated)." - }, - "role": { - "$ref": "#/components/schemas/MessageRole" - }, - "content": { + "fields": { "type": "array", "items": { - "$ref": "#/components/schemas/Content" + "$ref": "#/components/schemas/SummaryViewField" }, - "description": "The message content as an array of typed content parts." - }, - "createdAt": { - "type": "string", - "description": "Client-supplied timestamp for when the event happened (UTC).", - "format": "date-time" - }, - "systemTimestamp": { - "type": "string", - "description": "Server-set timestamp for when the dataplane ingested the event (UTC).", - "format": "date-time" - }, - "metadata": { - "description": "Optional metadata as any valid JSON value." + "maxItems": 50, + "minItems": 1 } }, "required": [ - "actorId", - "content", - "createdAt", - "eventId", - "role", - "sessionId", - "systemTimestamp" + "fields" ] }, - "SessionIdFilter": { + "SummaryViewScope": { "type": "object", - "description": "Filter by the long-term memory sessionId field. Matching is case-sensitive.", + "description": "An exact partition scope. The service requires exactly the fields configured by groupBy; singleton views accept none.", "properties": { - "eq": { + "ownerId": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Equals the given value." + "description": "Actor/owner identifier (1-64 chars, alphanumeric and dashes). Used for users, agents, or other participants." }, - "ne": { + "sessionId": { "type": "string", "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Not equal to the given value." - }, - "in": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." - }, - "description": "Matches any of the given values." - }, - "all": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." - }, - "description": "Matches all of the given values." - } - } - }, - "SessionSummary": { - "type": "object", - "description": "Compacted summary of older session history for a session.\n\nThis is derived session state maintained by the background summarization workflow, not a conversation event. It is present only after at least one summarization cycle has completed.", - "properties": { - "text": { - "type": "string", - "description": "The summary text, a single coherent block." + "description": "Session identifier (1-64 chars, alphanumeric and dashes). When the server generates one, it uses a 32-character UUID without dashes." }, - "summarizedUpToEventId": { - "type": "string", - "maxLength": 32, - "minLength": 32, - "pattern": "^[a-zA-Z0-9]+$", - "description": "Boundary event ID. This event and all events before it (by array position) are covered by the summary; `events` contains the retained turns after this boundary." - }, - "createdAt": { + "namespaceRef": { "type": "string", - "description": "Timestamp when the summary was first created (UTC). Preserved across re-summarization cycles.", - "format": "date-time" + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Server-generated namespace identifier." }, - "updatedAt": { + "memoryType": { "type": "string", - "description": "Timestamp when the summary was last regenerated (UTC). Updated on every summarization cycle. Equal to createdAt on the first cycle.", - "format": "date-time" - }, - "metadata": { - "description": "Metadata as a JSON object (e.g., model name, token count). Always present; defaults to an empty object when no metadata was supplied." - }, - "summarizedEvents": { - "type": "integer", - "minimum": 1, - "description": "Cumulative number of session events folded into this summary across all summarization cycles. Always positive. A value of 1 may indicate a summary created before this field was tracked; it self-corrects to a real count on the next summarization cycle.", - "format": "int64" + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Identifier of a long-term memory type.\n\nBuilt-in types include `semantic`, `episodic`, and `message`.\nStores may also register custom memory types; this open string accepts any\nregistered type name.\n\nConstraints: 1-64 chars, must start with a letter, followed by letters, digits,\nunderscores, or dashes." } - }, - "required": [ - "createdAt", - "metadata", - "summarizedEvents", - "summarizedUpToEventId", - "text", - "updatedAt" - ] + } }, - "StoreHealthFeature": { - "type": "object", - "description": "Feature-level store health.", - "properties": { - "status": { - "$ref": "#/components/schemas/StoreHealthStatus" - } - }, - "required": [ - "status" + "SummaryViewSource": { + "type": "string", + "description": "The source record family summarized by a Summary View. The MVP supports long-term memory only.", + "enum": [ + "long_term" ] }, - "StoreHealthFeatureMap": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/StoreHealthFeature" - } - }, - "StoreHealthResponseContent": { - "type": "object", - "description": "Store-scoped RAM feature health.", - "properties": { - "status": { - "$ref": "#/components/schemas/StoreHealthStatus" - }, - "features": { - "$ref": "#/components/schemas/StoreHealthFeatureMap" - } - }, - "required": [ - "features", - "status" + "SummaryViewState": { + "type": "string", + "description": "Summary View definition lifecycle state.", + "enum": [ + "active", + "disabled", + "deleting" ] }, - "StoreHealthStatus": { + "SummaryViewVisibility": { "type": "string", - "description": "Store health status.", + "description": "The immutable audience of a Summary View.", "enum": [ - "healthy", - "degraded", - "unavailable" + "PRIVATE", + "SHARED" ] }, "TimeoutErrorResponseContent": { @@ -3299,7 +9384,11 @@ "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "The caller's namespace. The update is rejected with 409 unless the record's stored namespace matches exactly.\n\nOmit it to assert that the record has no namespace — an omitted value matches only records stored without one, it does not mean \"any namespace\". The check is applied atomically with the update, so a record cannot be re-filed into another namespace between the check and the write." + "description": "The caller's current deprecated namespace label. The update is rejected with 409 unless the record still stores that label. Do not supply it with namespaceRef.\n\nOmit both namespace and namespaceRef to assert that the record has no namespace placement. Omission does not mean \"any namespace\".", + "deprecated": true + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRefInput" }, "text": { "type": "string", @@ -3345,7 +9434,11 @@ }, "namespace": { "type": "string", - "description": "Updated namespace. When provided, must be 1-64 chars alphanumeric and dashes; omit the field or send an empty string to clear.\n\nThis is the only way to change the namespace of an existing record, including one produced by extraction: a record inherits its session's namespace at creation and is not revised afterwards, so use this to re-file a record or to add a namespace to one created before the session had any." + "description": "Updated deprecated namespace label. When provided, it must be 1-64 alphanumeric characters or dashes. Send an empty string to clear it.\nDo not supply it with namespaceRef.\n\nThis shape is deprecated: Use namespaceRef to reference a namespace resource.", + "deprecated": true + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRefInput" }, "ownerId": { "type": "string", @@ -3398,7 +9491,11 @@ "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z0-9-]+$", - "description": "Namespace for logical grouping. Absent when the record has none: either it was written directly without one, or it was extracted from a session that has no namespace." + "description": "Deprecated namespace label. Absent when the record uses namespaceRef or has no namespace.", + "deprecated": true + }, + "namespaceRef": { + "$ref": "#/components/schemas/NamespaceRef" }, "topics": { "type": "array", @@ -3423,6 +9520,21 @@ "type": "string", "description": "Timestamp when the memory was last updated (UTC).", "format": "date-time" + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemorySourceLink" + }, + "description": "Generic source links for this memory. Document sources include the customer document ID and revision that produced the memory." + }, + "sourcesNextPageToken": { + "type": "string", + "description": "Opaque token for additional source links when this response truncated attribution. Absent when every link is included." + }, + "documentManaged": { + "type": "boolean", + "description": "True when this memory was created by document ingestion and is protected from generic overwrite." } }, "required": [ @@ -3431,6 +9543,181 @@ "text", "updatedAt" ] + }, + "UpdateNamespaceRequestContent": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "One case-sensitive namespace path segment." + }, + "state": { + "$ref": "#/components/schemas/NamespaceUpdateState" + } + } + }, + "UpdateNamespaceResponseContent": { + "type": "object", + "properties": { + "namespace": { + "$ref": "#/components/schemas/Namespace" + } + }, + "required": [ + "namespace" + ] + }, + "UpdateSummaryViewRequestContent": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "description": "Customer-visible Summary View name (1-100 characters)." + }, + "description": { + "type": "string", + "maxLength": 200, + "description": "New description. Send an empty string to clear it." + }, + "filter": { + "$ref": "#/components/schemas/LongTermMemoryFilter" + }, + "clearFilter": { + "type": "boolean", + "description": "Clear the saved filter. Mutually exclusive with filter." + }, + "filterOp": { + "$ref": "#/components/schemas/FilterConjunction" + }, + "groupBy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SummaryViewGroupField" + }, + "maxItems": 4 + }, + "evictionPolicy": { + "$ref": "#/components/schemas/SummaryViewEvictionPolicy" + }, + "timeWindowSeconds": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "clearTimeWindow": { + "type": "boolean", + "description": "Clear the rolling time window. Mutually exclusive with timeWindowSeconds." + }, + "output": { + "$ref": "#/components/schemas/SummaryViewOutputConfig" + }, + "refreshPolicy": { + "$ref": "#/components/schemas/SummaryViewRefreshPolicy" + }, + "state": { + "$ref": "#/components/schemas/MutableSummaryViewState" + } + } + }, + "UpdateSummaryViewResponseContent": { + "type": "object", + "description": "An owned Summary View definition. Results are separate derived state and never enter long-term-memory search.", + "properties": { + "id": { + "type": "string", + "maxLength": 32, + "minLength": 32, + "pattern": "^[a-zA-Z0-9]+$", + "description": "Server-generated Summary View identifier." + }, + "ownerId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Immutable canonical user ID assigned by the server. Clients cannot set ownership." + }, + "visibility": { + "$ref": "#/components/schemas/SummaryViewVisibility" + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "description": "Customer-visible Summary View name (1-100 characters)." + }, + "description": { + "type": "string", + "maxLength": 200, + "description": "Optional Summary View description (up to 200 characters)." + }, + "source": { + "$ref": "#/components/schemas/SummaryViewSource" + }, + "filter": { + "$ref": "#/components/schemas/LongTermMemoryFilter" + }, + "filterOp": { + "$ref": "#/components/schemas/FilterConjunction" + }, + "groupBy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SummaryViewGroupField" + }, + "maxItems": 4 + }, + "evictionPolicy": { + "$ref": "#/components/schemas/SummaryViewEvictionPolicy" + }, + "timeWindowSeconds": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "output": { + "$ref": "#/components/schemas/SummaryViewOutputConfig" + }, + "refreshPolicy": { + "$ref": "#/components/schemas/SummaryViewRefreshPolicy" + }, + "definitionVersion": { + "type": "integer", + "minimum": 1, + "format": "int64" + }, + "state": { + "$ref": "#/components/schemas/SummaryViewState" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "definitionVersion", + "filterOp", + "groupBy", + "id", + "name", + "output", + "ownerId", + "refreshPolicy", + "source", + "state", + "updatedAt", + "visibility" + ] } } } diff --git a/content/develop/ai/context-engine/agent-memory/developer-guide.md b/content/develop/ai/context-engine/agent-memory/developer-guide.md index f0520ec30d..964f1dd1b1 100644 --- a/content/develop/ai/context-engine/agent-memory/developer-guide.md +++ b/content/develop/ai/context-engine/agent-memory/developer-guide.md @@ -8,174 +8,218 @@ description: Connect an application to Redis Agent Memory and work with session hideListLinks: true linktitle: Developer guide title: Redis Agent Memory developer guide -weight: 5 +weight: 6 --- -Use the Python SDK, TypeScript SDK, or REST API to add session events, retrieve conversation context, create long-term memories, and search for relevant information. +Your application brings two kinds of memory to a conversation: the messages exchanged so far and information worth remembering from earlier conversations. Redis Agent Memory stores both. Your application retrieves them, passes them to the model, and saves each new turn. -## Integration workflow +This guide uses a travel assistant to show where those requests belong in your application. Complete the [quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}) first to create a service and send your first requests. For how the two kinds of memory work, see the [overview]({{< relref "/develop/ai/context-engine/agent-memory/overview" >}}). -1. Connect to a Redis Agent Memory service with its endpoint, Store ID, and API key. -1. Add conversation events to session memory. -1. Retrieve session memory before an agent turn to reconstruct the conversation context. -1. Search long-term memory for information relevant to the current interaction. + -Redis Agent Memory can automatically summarize older session events and extract long-term memories in the background. Applications can also create long-term memories directly. +## Configure the application -## Choose a client +Every client needs the service endpoint, Store ID, and application programming interface (API) key. The endpoint tells the client where to connect. The Store ID selects the memory store, and the key authenticates the request. -| Client | Use it when | Package and quickstart | -|:-------|:------------|:-----------------------| -| Python SDK | Your application or agent uses Python. | Install [`redis-agent-memory`](https://pypi.org/project/redis-agent-memory/) and follow the [Python SDK quickstart](/content/develop/ai/context-engine/agent-memory/python-sdk-quickstart.md). | -| TypeScript SDK | Your application or agent uses JavaScript or TypeScript. | Install [`@redis-iris/agent-memory`](https://www.npmjs.com/package/@redis-iris/agent-memory) and follow the [TypeScript SDK quickstart](/content/develop/ai/context-engine/agent-memory/typescript-sdk-quickstart.md). | -| REST API | You need language-independent HTTP access or don't want an SDK dependency. | No package required. Follow the [REST API quickstart](/content/develop/ai/context-engine/agent-memory/rest-api-quickstart.md). | +1. In the Redis Cloud console, open your Agent Memory service and select **Configuration**. +1. Copy the **Endpoint** and **Store ID** into your application's configuration. +1. Load the API key you saved during service creation from a secret store or environment variable. Keep it out of source control and browser code. +1. Create the client as shown in [Create the client and check the service health]({{< relref "/develop/ai/context-engine/agent-memory/quickstart#create-the-client-and-check-the-service-health" >}}). Use the Python software development kit (SDK) for Python applications, the TypeScript SDK for JavaScript or TypeScript, or the REST API for other languages. -## Connect to a Redis Agent Memory service +### Manage a service with the Control Plane SDK -Every client requires: +For store and extractor administration, see [Manage a service with the Control Plane SDK](/content/operate/iris/agent-memory/self-managed/api-examples.md#manage-a-service-with-the-control-plane-sdk). Keep operator credentials separate from application credentials. -* The Redis Agent Memory endpoint. -* The Store ID. -* A Redis Agent Memory API key. +## Identify users and conversations -The Python and TypeScript SDKs accept the endpoint, Store ID, and API key when you create the client. When you use the REST API, send the API key as a bearer token and include the Store ID in request paths. +Use one stable account ID for the current user across conversations. Get it from your application's authenticated identity, such as its login session or a verified identity-provider claim. An external identity provider is not required when your backend handles user authentication and authorization. Do not generate a new user ID for each chat or trust an owner ID supplied by a browser. -Follow the [Redis Cloud setup guide](/content/operate/iris/agent-memory/create-service.md) if you don't have a service. After you create one, copy its endpoint and Store ID from the **Configuration** tab and save the API key securely. +Some IDs come from your application; others come from Redis Agent Memory: -## Identify users and conversations +| Identifier | Who supplies it | How to use it | +|:-----------|:----------------|:--------------| +| `sessionId` | Your application, or the server if you omit it when adding the first event. | Save the returned `event.sessionId` and reuse it for later turns. A new chat needs a new ID. | +| `actorId` | Your application, on every session event. | Use the current user's stable ID on the first event. Later messages can use an assistant ID, such as `travel-agent`. | +| Session `ownerId` | The server copies it from the first event's `actorId`. | There is no separate `ownerId` input on a session-event request. Later actors do not change the session owner. | +| Long-term memory `ownerId` | Your application for direct creation; extraction inherits the session owner. | Use the current user's stable ID for personal memories and owner filters. | +| `eventId` | The server. | Save it from the event response if you need to refer to that event. | +| Long-term memory `id` | Your application for direct creation; the server during extraction. | Supply a stable record ID when creating a memory directly. | +| `namespaceId` | The server when it creates a namespace. | Save it and use it in `namespaceRef`. Supply the user's `ownerId` when creating a personal namespace. | +| Event `createdAt` | Your application. | Send when the message occurred; it is a required event input. | +| Event `systemTimestamp` | The server. | Records when the event was stored. | +| Memory `createdAt` and `updatedAt` | The server. | Use the returned timestamps when displaying or sorting memories. | -| Identifier | Purpose | -|:-----------|:--------| -| `sessionId` | Identifies a conversation or interaction session. | -| `actorId` | Identifies the actor that produced a session event. | -| `ownerId` | Identifies the user or entity associated with a long-term memory. | -| Memory ID | Uniquely identifies a long-term memory within the store. | +For example, the quickstart uses `quickstart-user` as the user's ID and `travel-planning-session` for one conversation. Replace the user ID with your authenticated account ID in an application. IDs must follow the format in the [API reference]({{< relref "/develop/ai/context-engine/agent-memory/api-reference" >}}). -An application can use the same user identifier for `actorId` and `ownerId`, but the fields describe different relationships. +For on-prem deployments, see [Server-side ownership enforcement](/content/operate/iris/agent-memory/self-managed/authentication.md#server-side-ownership-enforcement) for identity requirements and configuration. -## Work with session memory +### Authorize every memory request -Use a stable `sessionId` to store a conversation as an ordered sequence of events. Add an event for each user, assistant, or system message that your application needs to retain. +Your application must decide which memories the user can access. Put these checks in trusted backend code before sending a request to Redis Agent Memory. Store access alone does not restrict a caller to one user's memories. Cloud store credentials also require these application checks; a Redis Cloud account user is not your application's end user. -Each stored event can include: +* For personal searches, set the `ownerId` filter from the authenticated user's ID. For session listing, use `filterOwnerId`. A request from `user-42` must not be able to select `user-99` as its owner. +* For shared memories, check which shared namespaces that user can access, then restrict the query to those namespace IDs. Do not add a personal owner filter to that shared query: it would hide shared memories written by other users. +* For requests that retrieve or change a record by ID, check that the record belongs to the user or to a shared namespace they can access. These endpoints do not all accept an owner filter; a session or memory ID is not proof of permission. -| Field | Purpose | -|:------|:--------| -| `eventId` | Server-generated identifier for the event. | -| `sessionId` | Session that contains the event. | -| `actorId` | User, agent, or other actor that produced the event. | -| `role` | Role of the message in the conversation. | -| `content` | Message content, including its text. | -| `createdAt` | Time the event occurred in the application. | -| `systemTimestamp` | Time Redis Agent Memory stored the event. | -| `metadata` | Optional application-specific information associated with the event. | +Use the authenticated user on every request to choose the owner filter or the permitted shared namespaces. Search filters limit results; they do not authenticate the user. For examples, see [Namespaces](/content/develop/ai/context-engine/agent-memory/namespaces/_index.md). -Before an agent turn, retrieve the session by `sessionId` and provide the relevant events to the agent. This lets the application reconstruct the conversation without maintaining a separate conversation store. +Keep mandatory owner restrictions separate from optional search conditions. Combining an owner and another filter with `filterOp: "any"` matches either condition; it does not require the owner to match. `includeAll` on a list request is also a selection option, not permission to enumerate a store. -### Session retention +## Build context for each agent turn -The session-memory TTL controls how long sessions remain available. Configure it according to the retention requirements of your application. When a session expires, its events are no longer available through session-memory retrieval. +Suppose a user returns to the travel assistant and asks, "Where should I eat in Kyoto?" The current conversation may contain their travel dates. Long-term memory may contain their vegetarian diet from an earlier chat. The model needs both to give a useful answer. -See [memory configuration](/content/operate/iris/agent-memory/create-service.md#memory-configuration) to configure the session-memory TTL in Redis Cloud. +1. Retrieve session memory with the conversation's `sessionId`. +1. Search long-term memory using the user's request and an `ownerId` filter. +1. Build the model context from the session summary, recent events, relevant search results, and the current user message. +1. Call the model to produce the assistant's response. +1. Store the new user and assistant messages as session events, with their roles and actor IDs. -### Automatic session summarization +The following Python example prepares that context. It uses the client, `SESSION_ID`, and `USER_ID` from the quickstart. Add it inside the `with` block in `main`, after the health check, and run `python quickstart.py`. It reads existing memory and prints the context without adding an event. -Automatic summarization limits the amount of conversation history that must be added to a model's context window. Configure: +```python + import json -* **Summarize after:** The number of messages a session can contain before older messages are summarized. -* **Keep most recent:** The number of recent messages that remain available in full. + question = "Where should I eat in Kyoto?" + session = agent_memory.get_session_memory(session_id=SESSION_ID) + memories = agent_memory.search_long_term_memory( + request={ + "text": question, + "filter_": {"owner_id": {"eq": USER_ID}}, + "limit": 5, + }, + ) -For example, with **Summarize after** set to 20 and **Keep most recent** set to 10, Redis Agent Memory summarizes the older 10 messages when the session reaches 20 messages and retains the 10 most recent messages in full. + context = { + "session_summary": session.summary.text if session.summary else None, + "recent_events": [ + event.model_dump(mode="json", by_alias=True) + for event in session.events + ], + "relevant_memories": [memory.text for memory in memories.items], + "current_message": question, + } + print(json.dumps(context, indent=2)) +``` -See [automatic summarization](/content/operate/iris/agent-memory/create-service.md#automatic-summarization) to enable summarization and configure both thresholds in Redis Cloud. +Pass this information to your model through your application's existing model call. Keep conversation roles and treat recalled memories as context, not as instructions. For a complete application example, use the [AI agent builder]({{< relref "/develop/ai/agent-builder" >}}) and select **Redis Iris Conversational Assistant**. -Follow any of the client quickstarts to add and retrieve a session event. For complete schemas, see the [session-memory API reference](/content/develop/ai/context-engine/agent-memory/api-reference.md#tag/session-memory). +After the model responds, use the [session-event request]({{< relref "/develop/ai/context-engine/agent-memory/quickstart#1-build-conversation-context-with-session-memory" >}}) to save each new message. Save the question with role `USER` and the user's actor ID. Save the answer with role `ASSISTANT` and actor ID `travel-agent`. Both use the same session ID. -## Work with long-term memory +In this sequence, the question appears once in `current_message` because it has not been saved yet. If you save it before retrieving the session, it will already be in `recent_events`; omit `current_message` in that case. -Long-term memory stores information that remains useful beyond one conversation. A long-term memory record includes content and fields that let applications classify, scope, and retrieve it: +The context must fit the model's input limit. Count tokens with your model provider's tokenizer and leave room for the answer. The example limits recall to five memories. If the prompt is still too large, use fewer results or enable [session summarization]({{< relref "/develop/ai/context-engine/agent-memory/sessions#configure-automatic-summarization" >}}). When summarization is enabled, session retrieval returns the summary and recent events separately; the example includes both. -| Field | Purpose | -|:------|:--------| -| `id` | Unique identifier for the memory. | -| `text` | Memory content used for retrieval. | -| `memoryType` | Built-in or custom memory type. | -| `sessionId` | Session associated with the memory. | -| `ownerId` | User or entity associated with the memory. | -| `namespace` | Logical grouping for the memory. | -| `topics` | Topic tags used to categorize the memory. | -| `createdAt` | Time the memory was created. | -| `updatedAt` | Time the memory was last updated. | +## Handle long-term recall -### Create long-term memories +Extraction runs in the background. Saving "I am vegetarian" as a session event makes the message available in session memory, but an extracted preference may not be searchable yet. The context example includes the session events, so the model can use that statement on the next turn without waiting for extraction. -Redis Agent Memory provides two creation paths: +When a search returns no matches, `items` is an empty array. In the example, `relevant_memories` becomes `[]`; the session history and current question remain available. If you expect a memory but it does not appear: -* **Automatic extraction:** Redis Agent Memory processes session events asynchronously and creates durable memories from relevant information. Configure the extraction cadence to control how often session events are processed. -* **Direct creation:** Your application creates one or more long-term memories through the API or an SDK. Use direct creation when importing existing information or when your application determines exactly what to store. +1. Open the service's **Configuration** tab in Redis Cloud and check **Extraction cadence** under **Memory configuration**. Wait at least that interval after adding the event, then search again. +1. If you expect a custom memory type, check that it is enabled under **Memory types & extraction**. Check that its extraction prompt covers the information in the event. +1. Compare the request's `ownerId`, `memoryType`, and namespace filters with the IDs and type used for the memory. Start with the owner filter alone to check whether an added filter excludes it. -Configure the long-term-memory TTL separately from the session-memory TTL. +See [View and edit service configuration]({{< relref "/operate/iris/agent-memory/view-service#configuration-tab" >}}) for the settings. Extraction uses a model, so it may not create a memory for every statement. -See [memory configuration](/content/operate/iris/agent-memory/create-service.md#memory-configuration) to configure the extraction cadence and long-term-memory TTL in Redis Cloud. +A failed request is different from a successful search with no matches. Handle the returned error before you use the result: -### Exclude sensitive data from automatic extraction +### Handle typed errors and partial results -Sensitive-data exclusions keep specified information out of long-term memory during automatic extraction. A store's exclusions policy combines three mechanisms that you enable independently: +| What you see | What it means | What to do | +|:-------------|:--------------|:-----------| +| Empty `items` | No memories match yet. | Continue with session context. | +| Invalid request | A field or filter is invalid. | Correct it using the error details. | +| `401` | The credential is invalid or expired. | Correct the key or refresh the user's token. | +| `403` or a hidden personal-record `404` | Store grants, or on-prem ownership enforcement, prevent the request. | Check grants and authenticated identity. An owner filter cannot grant access. | +| `409` | A resource conflicts, such as an archived destination or stale revision. | Correct the destination or read the current revision before retrying. | +| `423` | The resource is suspended. | Restore its availability. | +| `424` | A dependency failed. | Inspect the typed error and problem type. | +| `429` or retryable `5xx` | The request is throttled or temporarily unavailable. | Use bounded backoff and honor retry timing. | +| Search timeout | Recall did not complete. | Use session context if it is enough to answer. Otherwise, report memory unavailability. | +| Event-write timeout | The write may already have succeeded. | Retrieve the session before retrying; repeated writes can add duplicate events. | +| Successful bulk response with `errors` | Some records failed; others may already be changed. | Inspect every item before deciding what to retry. | -* **Built-in detectors:** Validated patterns for common identifiers, such as payment card numbers, email addresses, phone numbers, IP addresses, and US Social Security numbers. Select the ones a store should apply. -* **Custom detectors:** Regular expressions you write yourself, for identifiers specific to your domain such as tenant IDs or internal reference numbers. -* **Semantic exclusions:** A plain-language exclusion prompt describing what must not be kept, such as passwords, access tokens, recovery codes, payment card information, or booking confirmation codes. Use it for concepts a pattern cannot express. +Before retrying a write, follow its idempotency or recovery contract. -Every detector chooses what happens to a memory it matches: redact the matched text and keep the rest, or drop the memory. When a memory matches several detectors that choose different actions, the memory is dropped. +The operation's response contract determines its `424` exception class. For example, search uses `MemoryFailedDependencyErrorResponseContent`, while store health uses `FailedDependencyErrorResponseContent`. Both can report unavailable resources or database memory exhaustion; the memory response also covers model credentials and quota failures. Inspect the problem type as well as the class. Python exceptions are in `redis_agent_memory.errors`, with problem fields under `error.data`. TypeScript exceptions are in `@redis-iris/agent-memory/models/errors`. -Exclusions apply to automatic extraction from session events. They do not apply when an application creates long-term memories directly, and session memory itself is never altered. +This complete program checks store health before recall and handles either operation's dependency error separately from an empty `items` array: -> [!WARNING] -> Detector matches are deterministic, but semantic exclusions are advisory and do not guarantee exclusion. Sensitive session content still reaches the extraction model provider. Use appropriate controls before sending sensitive information to Redis Agent Memory or the model provider. +```python +import asyncio +import os + +from redis_agent_memory import AgentMemory, errors + + +async def main(): + headers = {"X-Iris-User-Token": os.environ["USER_TOKEN"]} if os.environ.get("USER_TOKEN") else {} + async with AgentMemory( + os.environ["AGENT_MEMORY_URL"], store_id=os.environ["STORE_ID"], + api_key=os.environ["API_KEY"], + ) as memory: + try: + await memory.store_health_async(http_headers=headers) + result = await memory.search_long_term_memory_async( + request={"filter_": {"owner_id": {"eq": os.environ["OWNER_ID"]}}, "limit": 5}, + http_headers=headers, + ) + except errors.MemoryFailedDependencyErrorResponseContent as error: + print("Memory dependency unavailable:", error.data.title) + return + except errors.FailedDependencyErrorResponseContent as error: + print("Service dependency unavailable:", error.data.title) + return + print([item.text for item in result.items]) + + +asyncio.run(main()) +``` -See [sensitive-data exclusions](/content/operate/iris/agent-memory/create-service.md#sensitive-data-exclusions) to configure the feature in Redis Cloud. +The ingestion-hook `503` retry error and `422` stopped error also have distinct types and recovery rules. See [webhook failures](/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md#handle-failures-and-inspect-status). To check the store, use `store_health` (Python) or `storeHealth` (TypeScript). -### Search long-term memory +## Create long-term memories -Search long-term memory using semantic, keyword, or hybrid retrieval. Scope results with filters for owners, sessions, namespaces, topics, and memory types. +Create memories directly when your application already has information to store, such as a preference submitted through a form. Set the owner and a stable record ID. Creating an eligible record with the same ID again replaces it; access checks and protection for document-managed records still apply. Check the bulk response for per-record errors. Direct creation does not apply automatic extraction or sensitive-data exclusions. -Use `ownerId` to restrict recall to the relevant user or entity. Add narrower filters when the application needs memories from a particular session, namespace, topic, or memory type. +See [Long-term memory]({{< relref "/develop/ai/context-engine/agent-memory/long-term-memory" >}}) for creation methods, search fields, retention, and extraction controls. -For request fields, filter operators, and response schemas, see [`SearchLongTermMemory`](/content/develop/ai/context-engine/agent-memory/api-reference.md#tag/long-term-memory/operation/SearchLongTermMemory). +For unstructured documents, use [MemoryETL](/content/develop/ai/context-engine/agent-memory/memory-etl.md) to configure an extractor, submit a revision, and track its outputs. Source links let you trace extracted memories back to document or session inputs. -### Define custom memory types +For a reusable summary of selected long-term memories, use [Summary Views](/content/develop/ai/context-engine/agent-memory/summary-views.md). Read their result status, freshness, and coverage before adding generated text or structured data to the model context. -Custom memory types capture structured information specific to your business domain. Define them in the Redis Agent Memory service configuration with: +Use [ingestion webhooks](/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md) when an application policy must review input or generated changes before they are saved. Configure the receiver, selected paths, signing keys, and retry handling before enabling the policy. -| Setting | Purpose | -|:--------|:--------| -| **Name** | Unique name used as the memory's `memoryType`. | -| **Description** | Description of the information represented by the type. | -| **Extraction prompt** | Instructions that tell Redis Agent Memory when and how to extract the custom memory from session events. | -| **Enabled** | Controls whether Redis Agent Memory extracts new memories for the type. | -| **Custom fields** | Structured fields added to memories of this type. | +## Define custom memory types -Custom fields support `str`, `int`, `float`, `bool`, `list[str]`, `list[float]`, and `object`. Each field has a name and description that explain what it captures. Every custom memory also includes the built-in long-term memory fields listed above. +If your application needs structured values, define a custom memory type. For example, `trip_preference` can capture `destinations`, `travel_period`, `dietary_requirements`, and `food_preferences`. -For example, a travel application could define a `trip_preference` type with these fields: +Enable the type and supply extraction instructions. Search with the `memoryType` and `ownerId` filters, then read each result's `attributes`. Check for missing fields before using them to choose restaurants. -| Field | Type | Captures | -|:------|:-----|:---------| -| `destination` | `str` | City or country the user plans to visit. | -| `travel_period` | `str` | Dates or period of the trip. | -| `dietary_requirements` | `list[str]` | Dietary requirements that affect recommendations. | +See [Custom memory types]({{< relref "/develop/ai/context-engine/agent-memory/long-term-memory#custom-memory-types" >}}) to define fields, handle extraction results, and create records directly. -When this type is enabled, Redis Agent Memory can extract a structured `trip_preference` memory from relevant session events. Each enabled custom type processes session events independently. +## Organize memories with namespaces -See [custom memory types](/content/operate/iris/agent-memory/create-service.md#custom-memory-types) for configuration requirements and limits. +Namespaces are optional. Add them when your application needs to group conversations and memories by user, project, or team. -Follow any of the client quickstarts to create and search long-term memory. For complete schemas, see the [long-term-memory API reference](/content/develop/ai/context-engine/agent-memory/api-reference.md#tag/long-term-memory). +For example, create a personal `travel` namespace and save its `namespaceId`. Use that ID in `namespaceRef` when starting a session or creating long-term memories. You will also use this ID to search for memories in that namespace. Names and paths can change when you rename a namespace, but its ID stays the same. -## References +See [Namespaces]({{< relref "/develop/ai/context-engine/agent-memory/namespaces" >}}) for hierarchy, placement, search filters, and management. + +## Exclude sensitive data from automatic extraction + +Sensitive-data exclusions keep specified information out of long-term memory during automatic extraction. A store's exclusions policy combines three mechanisms that you enable independently: + +* **Built-in detectors:** Validated patterns for common identifiers, such as payment card numbers, email addresses, phone numbers, Internet Protocol (IP) addresses, and US Social Security numbers. Select the ones a store should apply. +* **Custom detectors:** Regular expressions you write yourself, for identifiers specific to your domain such as tenant IDs or internal reference numbers. +* **Semantic exclusions:** A plain-language exclusion prompt describing what must not be kept, such as passwords, access tokens, recovery codes, payment card information, or booking confirmation codes. Use it for concepts a pattern cannot express. + +Every detector chooses what happens to a memory it matches: redact the matched text and keep the rest, or drop the memory. When a memory matches several detectors that choose different actions, the memory is dropped. + +Exclusions apply to automatic extraction from session events. They do not apply when an application creates long-term memories directly, and session memory itself is never altered. + +> [!WARNING] +> Detector matches are deterministic, but semantic exclusions are advisory and do not guarantee exclusion. Sensitive session content still reaches the extraction model provider. Use appropriate controls before sending sensitive information to Redis Agent Memory or the model provider. -* [Python SDK quickstart](/content/develop/ai/context-engine/agent-memory/python-sdk-quickstart.md) -* [TypeScript SDK quickstart](/content/develop/ai/context-engine/agent-memory/typescript-sdk-quickstart.md) -* [Python SDK reference](https://pypi.org/project/redis-agent-memory/) -* [TypeScript SDK reference](https://www.npmjs.com/package/@redis-iris/agent-memory) -* [REST API quickstart](/content/develop/ai/context-engine/agent-memory/rest-api-quickstart.md) -* [Redis Agent Memory API reference](/content/develop/ai/context-engine/agent-memory/api-reference.md) +See [sensitive-data exclusions]({{< relref "/operate/iris/agent-memory/create-service#sensitive-data-exclusions" >}}) to configure the feature in Redis Cloud. diff --git a/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md b/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md new file mode 100644 index 0000000000..df29eb329f --- /dev/null +++ b/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md @@ -0,0 +1,345 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Configure blocking ingestion webhooks, verify signed callbacks, and recover from failures. +hideListLinks: true +linktitle: Ingestion webhooks +title: Review content with ingestion webhooks +weight: 15 +--- + +Ingestion webhooks let your application accept, reject, or transform content before Redis Agent Memory saves it. A callback returns a decision; successful HTTP delivery alone does not approve a write. + +Ingestion webhooks require a self-managed deployment with the Redis background-job backend. Hooks are off by default. + +## Choose what to review + +Select each path your policy needs. Selecting creation does not also select updates or deletion. + +### Before input is saved (`before_input`) + +| Path | What triggers it | Decisions | +|:-----|:-----------------|:----------| +| `session.append` | An application adds a session event. | Accept, reject, or transform. | +| `memory.create` | An application creates a memory batch. | Accept, reject, transform, or review each operation. | +| `document.submit` | An application submits a document for extraction. | Accept, reject, or transform. | +| `memory.update`, `memory.delete`, `memory.move` | An authorized caller changes, deletes, or moves a memory. | Accept or reject. | +| `session.summary`, `session.property` | An application or worker writes a session summary or property. | Accept or reject. | +| `session.delete`, `session.events.delete` | An application explicitly deletes a session or event. | Accept or reject. | + +### Before generated changes are applied (`before_apply`) + +| Path | What triggers it | Decisions | +|:-----|:-----------------|:----------| +| `session.builtin` | Built-in session extraction produces memory changes. | Whole-batch or per-operation decisions. | +| `session.custom` | Custom session extraction produces memory changes. | Whole-batch or per-operation decisions. | +| `document.chunk` | Document extraction produces memory changes from a chunk. | Whole-batch or per-operation decisions. | +| `summary_view.refresh` | A Summary View generates a value for publication. | Accept or reject. | + +Configuration, reads, searches, automatic expiration, and store teardown are outside content hooks. Empty extraction batches and empty Summary View results have no generated content to review. Authorization runs before content is sent to a receiver; an acceptance cannot grant access or bypass validation. + +## Prepare the deployment + +Ask your operator to [enable ingestion webhooks](/content/operate/iris/agent-memory/self-managed/configuration.md#enable-ingestion-webhooks) and configure the signing-secret backend. + +## Configure a store policy + +Your operator must [configure a store policy](/content/operate/iris/agent-memory/self-managed/api-examples.md#configure-a-webhook-policy), then provide the receiver with its signing keys and key IDs through a secret store. The receiver example reads them from `HOOK_KEYS_JSON`. Enable delivery after the receiver is ready. + +## Verify callbacks and return decisions + +The payload identifies `version`, `eventId`, `ingestionOperationId`, `phase`, `path`, store/owner context, configuration revision, correlation ID, and timestamp. Input appears under `input`; generated candidates appear under `operations`. A candidate's `operationId` identifies the proposed operation, separately from its target memory ID. + +Verify the exact raw request bytes before interpreting the JSON. The signature is: + +```text +v1= +``` + +Read the timestamp, event ID, key ID, and signature from `X-Redis-Webhook-Timestamp`, `X-Redis-Webhook-Event-Id`, `X-Redis-Webhook-Key-Id`, and `X-Redis-Webhook-Signature`. Check freshness and use a constant-time signature comparison. Retries keep the event ID and raw body but receive a fresh signed timestamp. + +This runnable receiver uses FastAPI and the Python standard library. It: + +1. Checks the four signature headers and the timestamp. +1. Verifies the hash-based message authentication code (HMAC) over the raw body. +1. Returns the saved decision if the event and body match a previous request. +1. Rejects content containing `[reject]` and accepts other signed content. + +Replace the demonstration marker policy with your application's policy. Save as `receiver.py`: + +```python +import base64 +import hashlib +import hmac +import json +import os +import time + +from fastapi import FastAPI, HTTPException, Request + +app = FastAPI() +keys = {} +for key_id, encoded in json.loads(os.environ["HOOK_KEYS_JSON"]).items(): + key = base64.b64decode(encoded, validate=True) + if not key_id or len(key) != 32 or base64.b64encode(key).decode("ascii") != encoded: + raise ValueError("Keys must be canonical base64 encodings of 32 bytes") + keys[key_id] = key +if not keys: + raise ValueError("Configure at least one signing key") +decisions = {} + + +def unique_object(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("Duplicate JSON key") + result[key] = value + return result + + +@app.post("/hook") +async def hook(request: Request): + names = ["X-Redis-Webhook-Key-Id", "X-Redis-Webhook-Event-Id", + "X-Redis-Webhook-Timestamp", "X-Redis-Webhook-Signature"] + if any(len(request.headers.getlist(name)) != 1 for name in names): + raise HTTPException(401, "Invalid signature headers") + key_id, event_id, stamp, signature = [request.headers[name] for name in names] + key = keys.get(key_id) + try: + fresh = abs(time.time() - int(stamp)) <= 300 + except ValueError: + fresh = False + if not key or not event_id or not fresh: + raise HTTPException(401, "Invalid signing identity or timestamp") + body = bytearray() + async for chunk in request.stream(): + body.extend(chunk) + if len(body) > 8 * 1024 * 1024: + raise HTTPException(413, "Body too large") + expected = "v1=" + hmac.new( + key, f"{stamp}.{event_id}.".encode() + body, hashlib.sha256, + ).hexdigest() + if not signature.isascii() or not hmac.compare_digest(signature, expected): + raise HTTPException(401, "Invalid signature") + try: + payload = json.loads(body, object_pairs_hook=unique_object) + except (ValueError, UnicodeDecodeError): + raise HTTPException(400, "Invalid JSON") + if not isinstance(payload, dict) or payload.get("version") != "1" or payload.get("eventId") != event_id: + raise HTTPException(400, "Invalid envelope") + if payload.get("phase") not in {"before_input", "before_apply"}: + raise HTTPException(400, "Invalid phase") + + # Demo only: one process and at most 1,000 saved decisions. + # Use a durable shared store before adding replicas or external effects. + now = time.time() + for identity, saved in list(decisions.items()): + if saved[2] <= now: + del decisions[identity] + identity = (key_id, event_id) + digest = hashlib.sha256(body).digest() + if identity in decisions: + saved_digest, decision, _ = decisions[identity] + if saved_digest != digest: + raise HTTPException(409, "Event content changed") + return decision + if len(decisions) >= 1000: + raise HTTPException(503, "Decision capacity reached") + decision = {"version": "1", "decision": "accept"} + if "[reject]" in json.dumps(payload, ensure_ascii=False): + decision.update(decision="reject", reasonCode="policy_denied") + decisions[identity] = (digest, decision, now + 7 * 24 * 60 * 60) + return decision +``` + +Install and run it with a certificate for the configured receiver hostname: + +```sh +uv venv +source .venv/bin/activate +uv pip install fastapi uvicorn +uvicorn receiver:app --host 0.0.0.0 --port 8443 \ + --ssl-certfile --ssl-keyfile +``` + +The example retains up to 1,000 decisions in one process for seven days. Restarting loses that cache. Use a durable shared store for production decisions and store the body fingerprint with each decision. Do not perform external side effects before saving an idempotent decision. + +Return HTTP `200` and `Content-Type: application/json` with a versioned decision. An empty body, bare success status, unknown fields, duplicate keys, or an invalid decision fails closed. Whole-request decisions are: + +```json +{"version":"1","decision":"accept"} +``` + +```json +{"version":"1","decision":"reject","reasonCode":"policy_denied"} +``` + +A document-input transform replaces its decoded text: + +```json +{"version":"1","decision":"transform","replacement":{"content":"Approved document text."}} +``` + +| Path | What a transform can change | +|:-----|:----------------------------| +| `document.submit` | The decoded document text in `replacement.content`. | +| `session.append` | `actorId`, `role`, `content`, `createdAt`, and `metadata`. Session identity, owner, and placement stay fixed. | +| `memory.create` | Editable text, type, session, owner, namespace placement, topics, and attributes. Record count, order, and IDs stay fixed. | + +Do not supply both `namespace` and `namespaceRef`. Transformed records must still pass authorization and validation. + +For `memory.create`, `session.builtin`, `session.custom`, and `document.chunk`, `decision: review` can decide each operation separately: + +```json +{ + "version":"1", + "decision":"review", + "operations":[ + {"operationId":"","decision":"accept"}, + {"operationId":"","decision":"transform","replacement":{"text":"Approved fact."}}, + {"operationId":"","decision":"reject","reasonCode":"policy_denied"} + ] +} +``` + +Return exactly one decision for every proposed operation; missing, duplicate, or unknown IDs invalidate the batch. Manual-create operation IDs are the supplied memory IDs. Generated candidates have separate operation IDs. Generated updates can transform editable fields; deletion can only be accepted or rejected. A receiver cannot add operations, change targets or kinds, or rewrite provenance. Native authorization, exclusions, namespace admission, and concurrency checks still apply. + +## Make input retries idempotent + +When selected, session append and manual memory creation require `idempotencyKey` containing 1–128 printable ASCII characters. In Python this is `idempotency_key`; TypeScript and REST use `idempotencyKey`. Document ingestion already requires this field. + +Save the complete original request, including timestamps, before sending it. After a lost response, reuse that request and key. Changing content under the same key conflicts. Saved acceptance and write receipts let a retry resume without repeating confirmed writes. A manual batch can still have per-record errors. + +To try the receiver's rejection marker, use a new memory ID and key with your authenticated owner. Save this synthetic request and repeat only the curl call to test an identical retry: + +```sh +export HOOK_TEST_MEMORY_ID="hook-test-$(openssl rand -hex 12)" +export HOOK_TEST_REQUEST_ID="hook-request-$(openssl rand -hex 12)" +jq -n --arg id "$HOOK_TEST_MEMORY_ID" --arg owner "$OWNER_ID" \ + --arg key "$HOOK_TEST_REQUEST_ID" \ + '{idempotencyKey:$key,memories:[{id:$id,ownerId:$owner,text:"[reject] synthetic webhook test"}]}' > hook-test.json +curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: ${USER_TOKEN:-}" \ + --header 'Content-Type: application/json' --data @hook-test.json \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory" | jq +``` + +With `memory.create` selected, inspect the per-record result and verify that the rejected record was not created. Use a new key for changed input; editing `hook-test.json` under the same key conflicts. + +The additional mutation paths and Summary View publication use fresh events rather than this input-replay contract. Retrying them reviews current state again; a lost response after a successful mutation can lead to another review. A callback's acceptance does not promise that its target will remain unchanged until persistence. + +## Handle failures and inspect status + +Callbacks have a five-second limit. Input callbacks have no automatic receiver retry. Interrupted undecided input can be resumed with the same original request and key; receiver failures remain terminal. Abandoned undecided input is cancelled after 60 seconds without a live lease. + +Session and document extraction output have at most three total callback attempts, with delays of 30 and 120 seconds within the job deadline. Saved candidates are resumed rather than regenerated. Explicit rejection filters candidates and permits progress; invalid responses or transport failures stop the current batch. Earlier batches may already be saved. + +For `summary_view.refresh`, a recorded rejection, invalid decision, or receiver failure stops that refresh's normal retries. Request a new refresh to generate a new proposal. Interrupted attempts can still be delivered again. Native generation or signing-key lookup failures have their own retry policy. + +Session summary and property hooks also apply to worker writes. A rejected summary is not saved and does not advance the worker's summary boundary; property hooks can also stop bookmark writes. + +| Response | Action | +|:---------|:-------| +| `503` with `/errors/ingestion-hook-retry` | Inspect `retryAt` and resume the same operation after that time. | +| `422` with `/errors/ingestion-hook-stopped` | Inspect the terminal state. Repeating the same stopped operation is not recovery. | +| `429` | Hook capacity is exhausted. Apply backoff and inspect pending work. | +| Bulk success with item errors | Check every record's result; HTTP success does not mean all records were applied. | + +Decision `state` and `applyState` are separate. Inspect `applied`, `filtered`, `conflicts`, attempts, safe errors, and retry times. A later failure or an incomplete completion record does not prove earlier writes were rolled back. You cannot manually retry a stopped operation through a retry endpoint. + +Use the Data Plane connection values from the [quickstart](/content/develop/ai/context-engine/agent-memory/quickstart.md). Set `HOOK_EVENT_ID` to inspect one known event, or omit it to list events. Add `USER_TOKEN` for delegated access: + +{{< multitabs id="hook-status" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python +import asyncio +import os + +from redis_agent_memory import AgentMemory + + +async def main(): + headers = {"X-Iris-User-Token": os.environ["USER_TOKEN"]} if os.environ.get("USER_TOKEN") else {} + async with AgentMemory( + os.environ["AGENT_MEMORY_URL"], store_id=os.environ["STORE_ID"], + api_key=os.environ["API_KEY"], + ) as memory: + if os.environ.get("HOOK_EVENT_ID"): + event = await memory.get_ingestion_hook_status_async( + event_id=os.environ["HOOK_EVENT_ID"], http_headers=headers, + ) + print(event.model_dump_json(by_alias=True)) + cursor = None + while True: + page = await memory.list_ingestion_hook_statuses_async( + limit=20, cursor=cursor, http_headers=headers, + ) + for status in page.statuses: + print(status.model_dump_json(by_alias=True)) + cursor = page.next_cursor + if not cursor: + break + + +asyncio.run(main()) +``` + +-tab-sep- + +```typescript +import { AgentMemory } from "@redis-iris/agent-memory"; + +async function main() { + const { AGENT_MEMORY_URL, STORE_ID, API_KEY, USER_TOKEN, HOOK_EVENT_ID } = process.env; + if (!AGENT_MEMORY_URL || !STORE_ID || !API_KEY) { + throw new Error("Set AGENT_MEMORY_URL, STORE_ID, and API_KEY."); + } + const memory = new AgentMemory({ serverURL: AGENT_MEMORY_URL, storeId: STORE_ID, apiKey: API_KEY }); + const headers: Record = USER_TOKEN ? { "X-Iris-User-Token": USER_TOKEN } : {}; + const options = { headers }; + if (HOOK_EVENT_ID) console.dir(await memory.getIngestionHookStatus(HOOK_EVENT_ID, options), { depth: null }); + let cursor: string | undefined; + do { + const page = await memory.listIngestionHookStatuses(20, cursor, options); + console.dir(page.statuses, { depth: null }); + cursor = page.nextCursor; + } while (cursor); +} + +main().catch((error) => { console.error(error instanceof Error ? error.message : "Request failed"); process.exitCode = 1; }); +``` + +-tab-sep- + +```sh +if [ -n "${HOOK_EVENT_ID:-}" ]; then + curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: ${USER_TOKEN:-}" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/ingestion-hooks/events/$HOOK_EVENT_ID" | jq +fi +cursor='' +while :; do + page=$(curl --fail-with-body --silent --show-error --get \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: ${USER_TOKEN:-}" \ + --data-urlencode 'limit=20' --data-urlencode "cursor=$cursor" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/ingestion-hooks/events") || exit 1 + printf '%s\n' "$page" | jq .statuses + cursor=$(printf '%s' "$page" | jq -r '.nextCursor // empty') + [ -n "$cursor" ] || break +done +``` + +{{< /multitabs >}} + +Lists return `statuses` and `nextCursor`. Pass `nextCursor` as `cursor`, even when the page is empty; ownership filtering can produce that result. Keep the same store, credential, and list parameters. The maximum page size is 100. + +Private pending content expires within 24 hours or the earlier operation deadline. Compact status and write receipts remain for seven days after completion or expiry. Status omits private input, generated content, prompts, endpoint configuration, and keys. + +Each store permits 10,000 hook records and 64 MiB of reserved pending content. Encoded callback requests and responses are limited to 8 MiB; decoded reviewed content is limited to 1 MiB. Hooks on bulk deletion or moves reduce the request limit to four memory IDs when that mutation path is selected; otherwise the native limit is 100. Split larger selected mutations into smaller requests and inspect every result. diff --git a/content/develop/ai/context-engine/agent-memory/interactive-demo.md b/content/develop/ai/context-engine/agent-memory/interactive-demo.md index e24a472693..8ac5be43e6 100644 --- a/content/develop/ai/context-engine/agent-memory/interactive-demo.md +++ b/content/develop/ai/context-engine/agent-memory/interactive-demo.md @@ -33,4 +33,4 @@ Try different messages to see how they change what the service remembers. Open * ## Next steps - [Create an Agent Memory service](/content/operate/iris/agent-memory/create-service.md) on Redis Cloud. -- Follow the [REST API quickstart](/content/develop/ai/context-engine/agent-memory/rest-api-quickstart.md) or the [Python SDK quickstart](/content/develop/ai/context-engine/agent-memory/python-sdk-quickstart.md) to try the same flow with a real service. +- Follow the [quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}) to try the same example with Python, TypeScript, or curl against a real service. diff --git a/content/develop/ai/context-engine/agent-memory/long-term-memory.md b/content/develop/ai/context-engine/agent-memory/long-term-memory.md new file mode 100644 index 0000000000..1ec0fa2e63 --- /dev/null +++ b/content/develop/ai/context-engine/agent-memory/long-term-memory.md @@ -0,0 +1,559 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Create and search memories that remain useful across conversations. +hideListLinks: true +linktitle: Long-term memory +title: Long-term memory +weight: 11 +aliases: +- /develop/ai/context-engine/agent-memory/custom-memory-types/ +--- + +Long-term memory stores information that remains useful beyond one conversation. Use automatic extraction or direct creation to store memories, then search for information relevant to an agent turn. + +## Create long-term memories + +Redis Agent Memory provides two ways to create memories: + +* **Automatic extraction:** Redis Agent Memory processes session events asynchronously and creates durable memories from relevant information. Configure the extraction cadence to control how often session events are processed. +* **Direct creation:** Your application creates one or more long-term memories through the application programming interface (API) or a software development kit (SDK). Use direct creation when importing existing information or when your application determines exactly what to store. + +Configure the long-term-memory time to live (TTL) separately from the session-memory TTL. + +See [memory configuration]({{< relref "/operate/iris/agent-memory/create-service#memory-configuration" >}}) to configure the extraction cadence and long-term-memory TTL in Redis Cloud. + +## Memory fields + +When you create a memory directly, you must supply `id`, `text`, and `ownerId`. The server sets `createdAt` and `updatedAt`. During automatic extraction, the server creates the memory ID and content and copies the session owner into `ownerId`. Use that same stable user ID when querying personal memories; see [Authorize every memory request](/content/develop/ai/context-engine/agent-memory/developer-guide.md#authorize-every-memory-request). + +A long-term memory record includes: + +| Field | Purpose | +|:------|:--------| +| `id` | Unique identifier for the memory. | +| `text` | Memory content used for retrieval. | +| `memoryType` | Built-in or custom memory type. | +| `sessionId` | Session associated with the memory. | +| `ownerId` | User or entity associated with the memory. | +| `namespaceRef` | Optional namespace ID and its current name and path. | +| `attributes` | Custom memory fields and their values. | +| `topics` | Topic tags used to categorize the memory. | +| `createdAt` | Time the memory was created. | +| `updatedAt` | Time the memory was last updated. | + +## Search long-term memory + +Search by meaning and use `ownerId` to restrict recall to the relevant user or entity. Add filters for a session, namespace, topic, or memory type as needed. + +For request fields, filter operators, and response schemas, see [`SearchLongTermMemory`]({{< relref "/develop/ai/context-engine/agent-memory/api-reference" >}}#operation/SearchLongTermMemory). + +For domain-specific fields and extraction instructions, see [Custom memory types]({{< relref "/develop/ai/context-engine/agent-memory/long-term-memory#custom-memory-types" >}}). To group memories in a hierarchy, see [Namespaces]({{< relref "/develop/ai/context-engine/agent-memory/namespaces" >}}). + +For controls that apply during automatic extraction, see [Exclude sensitive data from automatic extraction]({{< relref "/develop/ai/context-engine/agent-memory/developer-guide#exclude-sensitive-data-from-automatic-extraction" >}}). + +### Search by meaning + +Use the same connection values and owner ID as the [quickstart](/content/develop/ai/context-engine/agent-memory/quickstart.md). This example finds memories about the user’s food preferences: + +{{< clients-example set="agent_memory_sdk" step="search_long_term_memory_basic" footer="hide" dft_tab_name="curl" runnable="false" try_it="false" >}} +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data @- \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/search" <}} + +### Filter and sort without semantic search + +Omit `text` to retrieve records by filters without a semantic query. For example, retrieve the user's most recently updated memories: + +{{< multitabs id="filter-only-memory-search" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + recent_memories = agent_memory.search_long_term_memory(request={ + "filter_": {"owner_id": {"eq": USER_ID}}, + "sort_by": "updatedAt", + "sort_order": "desc", + "limit": 10, + }) + show("Recent memories", recent_memories) +``` + +-tab-sep- + +```typescript + const recentMemories = await agentMemory.searchLongTermMemory({ + filter: { ownerId: { eq: userId } }, + sortBy: "updatedAt", sortOrder: "desc", limit: 10, + }); + console.dir(recentMemories, { depth: null }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data "$(jq -n --arg owner "$OWNER_ID" \ + '{filter: {ownerId: {eq: $owner}}, sortBy: "updatedAt", sortOrder: "desc", limit: 10}')" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/search" | jq +``` + +{{< /multitabs >}} + +Use the client and identifiers from the [quickstart](/content/develop/ai/context-engine/agent-memory/quickstart.md). Add the Python snippet inside the `with` block and the TypeScript snippet inside `run`. + +`sortBy` accepts `createdAt` or `updatedAt` only when `text` is absent. `sortOrder` accepts `asc` or `desc`, defaults to `desc`, and requires `sortBy`. Semantic searches remain ranked by similarity and cannot use these sort fields. `limit` accepts 1–100 and defaults to 10. Pass `nextPageToken` as `pageToken` with the same query to continue. + +Top-level filters use `filterOp: "all"` (AND) by default; `"any"` uses OR. To restrict a query to a user, keep the owner condition mandatory. Do not use `"any"` with separate owner and namespace conditions when both must match. These retrieval filters do not replace application access checks. + +Set `filter.unnamespaced: true` to match records with neither the older `namespace` string label nor a namespace resource. It cannot be combined with `namespace` or `namespaceRef`, and remains mandatory even with `filterOp: "any"`. + +[MemoryETL](/content/develop/ai/context-engine/agent-memory/memory-etl.md) outputs are included in search by default. Set top-level `excludeDocumentManaged: true` to omit them. Returned `documentManaged` records cannot be overwritten through generic memory creation or PATCH requests; use the document-ingestion workflow to manage their content. + +For custom fields, use `filter.attributes`, keyed by the field name, with the operators described in [`AttributeFilterClause`](/content/develop/ai/context-engine/agent-memory/api-reference.md#operation/SearchLongTermMemory). Combine it with `memoryType` to select the registered custom type. + +For example, this search request combines the owner, type, and dietary requirement with `filterOp: "all"`. A `list` clause requires every listed value to be present: + +```json +{ + "filterOp": "all", + "filter": { + "ownerId": {"eq": ""}, + "memoryType": {"eq": "trip_preference"}, + "attributes": {"dietary_requirements": {"list": ["vegetarian"]}} + }, + "limit": 10 +} +``` + +Use exactly one typed clause per attribute: `string`, `number`, `boolean`, or `list`, matching the registered field type. + +## Custom memory types + +Custom memory types capture structured information specific to your business domain. + +### Define a memory type + +Configure the type on your Redis Agent Memory service with: + +| Setting | Purpose | +|:--------|:--------| +| **Name** | Unique name used as the memory's `memoryType`. | +| **Description** | Description of the information represented by the type. | +| **Extraction prompt** | Instructions that tell Redis Agent Memory when and how to extract the custom memory from session events. | +| **Enabled** | Controls whether Redis Agent Memory extracts new memories for the type. | +| **Custom fields** | Structured fields added to memories of this type. | + +Custom fields support `str`, `int`, `float`, `bool`, `list[str]`, `list[float]`, and `object`. Each field has a name and description that explain what it captures. Every custom memory also includes the built-in [long-term memory fields]({{< relref "/develop/ai/context-engine/agent-memory/long-term-memory#memory-fields" >}}). + +For example, a travel application can define a `trip_preference` type with these fields: + +| Field | Type | Captures | +|:------|:-----|:---------| +| `destinations` | `list[str]` | Cities or countries the user plans to visit. | +| `travel_period` | `str` | Dates or period of the trip. | +| `dietary_requirements` | `list[str]` | Dietary requirements that affect recommendations. | +| `food_preferences` | `list[str]` | Cuisines, flavors, or dining preferences. | + +When this type is enabled, Redis Agent Memory can extract a structured `trip_preference` memory from relevant session events. Each enabled custom type processes session events independently. + +### Extract custom memories + +{{< embed-md "agent-memory-continue-from-quickstart.md" >}} + +In the Redis Cloud console, open your service’s **Configuration** tab, select **Edit**, and under **Memory types & extraction** select **Add type**. Configure and enable `trip_preference` with the fields in [Define a memory type]({{< relref "/develop/ai/context-engine/agent-memory/long-term-memory#define-a-memory-type" >}}). Use this extraction prompt: + +```text +Extract trip requirements only when the user states a destination or travel plan. Preserve explicit dietary requirements and food preferences. +``` + +After enabling the type, add a travel message to a new session. Run this write once, then wait at least one extraction interval before searching. + +{{< multitabs id="custom-memory-event" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + agent_memory.add_session_event( + session_id="trip-preference-session", actor_id=USER_ID, + role=models.MessageRole.USER, + content=[models.Text(text="I am visiting Tokyo and Kyoto next month. I am vegetarian.")], + created_at=datetime.now(timezone.utc), + ) +``` + +-tab-sep- + +```typescript + await agentMemory.addSessionEvent({ + sessionId: "trip-preference-session", actorId: userId, role: "USER", + content: [{ text: "I am visiting Tokyo and Kyoto next month. I am vegetarian." }], + createdAt: new Date(), + }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data "$(jq -n --arg owner "$OWNER_ID" --arg now "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + '{sessionId:"trip-preference-session",actorId:$owner,role:"USER",content:[{text:"I am visiting Tokyo and Kyoto next month. I am vegetarian."}],createdAt:$now}')" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/events" | jq +``` + +{{< /multitabs >}} + +Search for the structured memory: + +{{< multitabs id="custom-memory-search" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + custom_results = agent_memory.search_long_term_memory( + request={ + "text": "What are the requirements for the user's trip?", + "filter_": { + "owner_id": {"eq": USER_ID}, + "memory_type": {"eq": "trip_preference"}, + }, + "limit": 5, + }, + ) + show("Trip preference memories", custom_results) +``` + +-tab-sep- + +```typescript + const customResults = await agentMemory.searchLongTermMemory({ + text: "What are the requirements for the user's trip?", + filter: { + ownerId: { eq: userId }, + memoryType: { eq: "trip_preference" }, + }, + limit: 5, + }); + console.log("Trip preference memories:"); + console.dir(customResults, { depth: null }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data @- \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/search" <}} + +The `items` array contains records with `memoryType` set to `trip_preference`. Custom fields are inside each record's `attributes` object. For example, a result can include this excerpt: + +```json +{ + "memoryType": "trip_preference", + "attributes": { + "destinations": ["Tokyo", "Kyoto"], + "travel_period": "next month", + "dietary_requirements": ["vegetarian"], + "food_preferences": ["spicy food"] + } +} +``` + +The values depend on the conversation and extraction model. An application can use `dietary_requirements` to constrain restaurant recommendations, while `food_preferences` helps rank suitable choices. Check that each field is present and has the expected type before using it. + +Inspect the structured fields. The SDK examples collect dietary requirements for the next agent turn: + +{{< multitabs id="custom-memory-fields" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + dietary_requirements = set() + if not custom_results.items: + print("No matching trip preferences yet. Retry after a short wait.") + for memory in custom_results.items: + attributes = memory.attributes or {} + requirements = attributes.get("dietary_requirements", []) + if isinstance(requirements, list): + dietary_requirements.update( + value for value in requirements if isinstance(value, str) + ) + print("Restaurant requirements:", sorted(dietary_requirements)) +``` + +-tab-sep- + +```typescript + const dietaryRequirements = new Set(); + if (customResults.items.length === 0) { + console.log("No matching trip preferences yet. Retry after a short wait."); + } + for (const memory of customResults.items) { + const requirements = memory.attributes?.dietary_requirements; + if (Array.isArray(requirements)) { + for (const value of requirements) { + if (typeof value === "string") dietaryRequirements.add(value); + } + } + } + console.log("Restaurant requirements:", [...dietaryRequirements].sort()); +``` + +-tab-sep- + +Run the search with this filter in place of the final `jq` command: + +```sh +jq 'if (.items | length) == 0 then + {status: "No matching trip preferences yet. Retry after a short wait."} + else + .items[] | {id, attributes} + end' +``` + +{{< /multitabs >}} + +If results remain empty, check that the type is enabled and that the owner ID matches the stored records. An empty result does not mean the user has no dietary requirements. + + +### Create a custom memory directly + +If your application already has structured trip data, write it directly using the registered `trip_preference` type. This optional example represents data from a form and uses the same user identifier: + +{{< multitabs id="direct-memory" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + direct_result = agent_memory.bulk_create_long_term_memories(memories=[{ + "id": "trip-form-1", + "text": "The user plans to visit Tokyo and Kyoto next month and requires vegetarian food.", + "owner_id": USER_ID, + "memory_type": "trip_preference", + "attributes": { + "destinations": ["Tokyo", "Kyoto"], + "travel_period": "next month", + "dietary_requirements": ["vegetarian"], + "food_preferences": ["spicy food"], + }, + }]) + show("Directly created trip preference", direct_result) +``` + +-tab-sep- + +```typescript + const directResult = await agentMemory.bulkCreateLongTermMemories({ + memories: [{ + id: "trip-form-1", + text: "The user plans to visit Tokyo and Kyoto next month and requires vegetarian food.", + ownerId: userId, + memoryType: "trip_preference", + attributes: { + destinations: ["Tokyo", "Kyoto"], + travel_period: "next month", + dietary_requirements: ["vegetarian"], + food_preferences: ["spicy food"], + }, + }], + }); + console.dir(directResult, { depth: null }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data @- \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory" <}} + +Inspect the bulk response for per-record errors. Run the custom-memory search again to retrieve the record. Direct creation does not wait for background extraction and does not apply the extraction prompt or sensitive-data exclusions. Creating an eligible record with the same `id` again replaces that record. Access checks and protection for document-managed records still apply. + +See [custom memory types]({{< relref "/operate/iris/agent-memory/create-service#custom-memory-types" >}}) for configuration requirements and limits. + +## Test semantic exclusions + +{{< embed-md "agent-memory-continue-from-quickstart.md" >}} + +In the Redis Cloud console, open your service’s **Configuration** tab and select **Edit**. Under **Sensitive-data exclusions**, enable **Semantic exclusions** and set this prompt: + +```text +Do not keep passwords, access tokens, recovery codes, payment card information, or booking confirmation codes in long-term memory. +``` + +The semantic exclusion prompt tells Redis Agent Memory which information should not be kept in long-term memory. Add an event containing a fictional booking code and information that is safe to retain: + +{{< multitabs id="exclusion-event" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + sensitive_event = agent_memory.add_session_event( + session_id=SESSION_ID, + actor_id=USER_ID, + role=models.MessageRole.USER, + content=[models.Text( + text=( + "I booked Hotel Sakura in Tokyo. For this example, " + "the fictional booking confirmation code is DEMO-7QX9." + ), + )], + created_at=datetime.now(timezone.utc), + ) + show("Event with excluded information", sensitive_event) +``` + +-tab-sep- + +```typescript + const sensitiveEvent = await agentMemory.addSessionEvent({ + sessionId, + actorId: userId, + role: "USER", + content: [{ + text: "I booked Hotel Sakura in Tokyo. For this example, the fictional booking confirmation code is DEMO-7QX9.", + }], + createdAt: new Date(), + }); + console.log("Event with excluded information:"); + console.dir(sensitiveEvent, { depth: null }); +``` + +-tab-sep- + +```sh +export EVENT_CREATED_AT="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data @- \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/events" <}} + +Run the event write once. In the SDK files, comment out event creation and its output statements before subsequent runs. Wait at least one minute and search for the safe hotel information: + +{{< multitabs id="exclusion-search" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + exclusion_results = agent_memory.search_long_term_memory( + request={ + "text": "Where is the user staying in Tokyo?", + "filter_": { + "owner_id": {"eq": USER_ID}, + }, + "limit": 5, + }, + ) + show("Memories after semantic exclusion", exclusion_results) +``` + +-tab-sep- + +```typescript + const exclusionResults = await agentMemory.searchLongTermMemory({ + text: "Where is the user staying in Tokyo?", + filter: { + ownerId: { eq: userId }, + }, + limit: 5, + }); + console.log("Memories after semantic exclusion:"); + console.dir(exclusionResults, { depth: null }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data @- \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/search" <}} + +> [!NOTE] +> **What to expect:** A memory similar to `User booked Hotel Sakura in Tokyo` without the fictional confirmation code `DEMO-7QX9`. If the code appears, refine the exclusion prompt and test again. + +> [!WARNING] +> Semantic exclusions are advisory and do not guarantee that sensitive information is excluded. Session content still reaches the extraction model provider. Do not use real sensitive data in this exercise. Exclusions do not apply to directly created long-term memories. + +See [sensitive-data exclusions]({{< relref "/operate/iris/agent-memory/create-service#sensitive-data-exclusions" >}}) for configuration details. diff --git a/content/develop/ai/context-engine/agent-memory/memory-etl.md b/content/develop/ai/context-engine/agent-memory/memory-etl.md new file mode 100644 index 0000000000..0c60959bee --- /dev/null +++ b/content/develop/ai/context-engine/agent-memory/memory-etl.md @@ -0,0 +1,356 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Extract long-term memories from documents, track processing, and inspect source evidence. +hideListLinks: true +linktitle: MemoryETL +title: Extract memories from documents with MemoryETL +weight: 13 +--- + +MemoryETL uses extract, transform, load (ETL) processing to turn text or Markdown documents into long-term memories. For example, a travel assistant can learn facts from a destination guide before a user starts a conversation. + +An **extractor** tells MemoryETL what to extract: a custom memory type and instructions for the model. Creating an extractor is an administrative task. Applications can then submit documents to it. + +Use [session extraction](/content/develop/ai/context-engine/agent-memory/sessions/_index.md) for conversation events. Use [direct memory creation](/content/develop/ai/context-engine/agent-memory/long-term-memory.md#create-a-custom-memory-directly) when your application already has the records it wants to save. + +## Prepare the service + +MemoryETL is available in Redis Cloud and enabled in self-managed deployments. These examples use a self-managed service. For its model, processing limits, and worker settings, see [Configure document ingestion](/content/operate/iris/agent-memory/self-managed/configuration.md#enable-document-ingestion). + +Install the SDK using the [quickstart](/content/develop/ai/context-engine/agent-memory/quickstart.md#install-the-sdk). Set: + +| Variable | Value | +|:---------|:------| +| `AGENT_MEMORY_URL`, `STORE_ID`, `API_KEY` | The Data Plane endpoint, store, and application credential. | +| `OWNER_ID` | Your application's authenticated user ID for this document. | +| `USER_TOKEN` | The user's token when self-managed ownership enforcement is enabled. See [delegation](/content/operate/iris/agent-memory/self-managed/authentication.md#delegate-a-user-with-an-agent-key). | + +## Create an extractor + +Ask your operator to [register a memory type and create a document extractor](/content/operate/iris/agent-memory/self-managed/api-examples.md#create-an-extractor). Set `EXTRACTOR_ID` to the returned extractor ID. Applications use their Data Plane credential to submit documents. + +## Submit and track a document + +Create `travel-guide.md` containing: + +```text +Kyoto has vegetarian restaurants near its central railway station. +``` + +Only `text/plain` and `text/markdown` are accepted. Encode the UTF-8 bytes as base64 **once** before passing `content` to REST or an SDK. The SDK expects the encoded string, not a file object or raw text. + +Each submission requires these fields: + +| Field | Purpose | Example | +|:------|:--------|:--------| +| `documentId` | Your ID for the logical document. | `travel-guide` | +| `revision` | An immutable, case-sensitive version. Use a new value when the content changes. | `1` | +| `content` | The file's UTF-8 bytes, base64-encoded once. | The examples encode `travel-guide.md`. | +| `mediaType` | The document's content type. | `text/plain` or `text/markdown` | +| `ownerId` | The authenticated user the memories belong to. | Your verified `OWNER_ID`. | +| `extractorId` | The extractor's returned `id`. | `$EXTRACTOR_ID` | +| `idempotencyKey` | A key to reuse when retrying the same submission. | `travel-guide-revision-1` | + +The server returns a separate ingestion ID to track the run. + +The examples submit the file, poll for a terminal processing state, and list output memory IDs. Save the ingestion ID even if polling times out: + +{{< multitabs id="document-ingestion" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python +import asyncio +import base64 +import os +from pathlib import Path + +from redis_agent_memory import AgentMemory + + +async def main(): + headers = {"X-Iris-User-Token": os.environ["USER_TOKEN"]} if os.environ.get("USER_TOKEN") else {} + async with AgentMemory( + os.environ["AGENT_MEMORY_URL"], store_id=os.environ["STORE_ID"], + api_key=os.environ["API_KEY"], + ) as memory: + accepted = await memory.create_document_ingestion_async( + document_id="travel-guide", revision="1", + content=base64.b64encode(Path("travel-guide.md").read_bytes()).decode("ascii"), + media_type="text/markdown", owner_id=os.environ["OWNER_ID"], + extractor_id=os.environ["EXTRACTOR_ID"], + idempotency_key="travel-guide-revision-1", http_headers=headers, + ) + ingestion_id = accepted.result.ingestion_id + print("Ingestion ID:", ingestion_id) + for _ in range(60): + record = await memory.get_document_ingestion_async( + ingestion_id=ingestion_id, http_headers=headers, + ) + status = record.ingestion.status + if status.processing in {"succeeded", "partial", "failed"}: + print(status.model_dump_json(by_alias=True)) + break + await asyncio.sleep(2) + else: + raise TimeoutError(f"Continue polling ingestion {ingestion_id}") + + offset = 0 + while True: + page = await memory.list_document_ingestion_outputs_async( + ingestion_id=ingestion_id, limit=100, offset=offset, http_headers=headers, + ) + if not page.memory_ids: + break + print(page.memory_ids) + offset += len(page.memory_ids) + if status.processing != "succeeded": + raise RuntimeError(f"Inspect ingestion {ingestion_id}: {status.processing}") + + +asyncio.run(main()) +``` + +-tab-sep- + +```typescript +import { readFile } from "node:fs/promises"; +import { setTimeout as delay } from "node:timers/promises"; +import { AgentMemory } from "@redis-iris/agent-memory"; + +async function main() { + const { AGENT_MEMORY_URL, STORE_ID, API_KEY, OWNER_ID, EXTRACTOR_ID, USER_TOKEN } = process.env; + if (!AGENT_MEMORY_URL || !STORE_ID || !API_KEY || !OWNER_ID || !EXTRACTOR_ID) { + throw new Error("Set the Data Plane values, OWNER_ID, and EXTRACTOR_ID."); + } + const memory = new AgentMemory({ serverURL: AGENT_MEMORY_URL, storeId: STORE_ID, apiKey: API_KEY }); + const headers: Record = USER_TOKEN ? { "X-Iris-User-Token": USER_TOKEN } : {}; + const options = { headers }; + const accepted = await memory.createDocumentIngestion({ + documentId: "travel-guide", revision: "1", + content: (await readFile("travel-guide.md")).toString("base64"), + mediaType: "text/markdown", ownerId: OWNER_ID, extractorId: EXTRACTOR_ID, + idempotencyKey: "travel-guide-revision-1", + }, options); + const ingestionId = accepted.result.ingestionId; + console.log("Ingestion ID:", ingestionId); + let finished = false; + let processing: string | undefined; + for (let attempt = 0; attempt < 60; attempt++) { + const { ingestion } = await memory.getDocumentIngestion(ingestionId, options); + if (["succeeded", "partial", "failed"].includes(ingestion.status.processing)) { + processing = ingestion.status.processing; + console.dir(ingestion.status, { depth: null }); + finished = true; + break; + } + await delay(2000); + } + if (!finished) throw new Error(`Continue polling ingestion ${ingestionId}`); + let offset = 0; + for (;;) { + const page = await memory.listDocumentIngestionOutputs(ingestionId, 100, offset, options); + if (!page.memoryIds.length) break; + console.log(page.memoryIds); + offset += page.memoryIds.length; + } + if (processing !== "succeeded") throw new Error(`Inspect ingestion ${ingestionId}: ${processing}`); +} + +main().catch((error) => { console.error(error instanceof Error ? error.message : "Request failed"); process.exitCode = 1; }); +``` + +-tab-sep- + +```sh +jq -n --rawfile text travel-guide.md \ + --arg owner "$OWNER_ID" --arg extractor "$EXTRACTOR_ID" \ + '{documentId:"travel-guide",revision:"1",content:($text|@base64),mediaType:"text/markdown",ownerId:$owner,extractorId:$extractor,idempotencyKey:"travel-guide-revision-1"}' > ingestion.json + +response=$(curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: ${USER_TOKEN:-}" \ + --header 'Content-Type: application/json' --data @ingestion.json \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/document-ingestions") || exit 1 +INGESTION_ID=$(printf '%s' "$response" | jq -er .ingestionId) || exit 1 +printf 'Ingestion ID: %s\n' "$INGESTION_ID" + +finished=false +for attempt in $(seq 1 60); do + status=$(curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: ${USER_TOKEN:-}" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/document-ingestions/$INGESTION_ID") || exit 1 + state=$(printf '%s' "$status" | jq -r .ingestion.status.processing) + case "$state" in + succeeded|partial|failed) printf '%s\n' "$status" | jq; finished=true; break ;; + esac + sleep 2 +done +[ "$finished" = true ] || { printf 'Continue polling %s\n' "$INGESTION_ID"; exit 1; } + +offset=0 +while :; do + page=$(curl --fail-with-body --silent --show-error --get \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: ${USER_TOKEN:-}" \ + --data-urlencode 'limit=100' --data-urlencode "offset=$offset" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/document-ingestions/$INGESTION_ID/outputs") || exit 1 + count=$(printf '%s' "$page" | jq '.memoryIds | length') + [ "$count" -gt 0 ] || break + printf '%s\n' "$page" | jq .memoryIds + offset=$((offset + count)) +done +[ "$state" = succeeded ] || { printf 'Inspect ingestion %s: %s\n' "$INGESTION_ID" "$state"; exit 1; } +``` + +{{< /multitabs >}} + +REST returns `202 Accepted`; this confirms submission, not successful extraction. In the SDKs, the submission content is under `accepted.result`, while the status read returns `ingestion.status`. Output listing uses `offset` and returns `memoryIds`. + +## Interpret results and recover + +| Processing state | Meaning and action | +|:-----------------|:-------------------| +| `queued` or `running` | Keep polling the saved ingestion ID. Do not create another submission to check progress. | +| `succeeded` | Processing finished. Inspect `createdCount` and `filteredCount`; success can produce no memories. | +| `partial` | Some chunks completed before a failure. Inspect `error`, counts, and available output IDs. Earlier writes remain. | +| `failed` | Inspect `error` and available outputs. Do not assume that every prior write was rolled back. | + +`completedChunks`, `totalChunks`, `extractorId`, and `extractorRevision` help identify what ran. `cleanup` is independent: `pending`, `deleted`, or `failed` describes temporary source cleanup. A successful extraction with failed cleanup needs operator attention; it is not a reason to submit the document again. Extracted memories and provenance can remain after source bytes are deleted. + +Reuse the same idempotency key and original request after a timeout or lost submission response. Changing the input under the same key conflicts. Reusing an immutable document revision with different content also conflicts. Use a new revision for changed content; do not change the bytes merely to force a retry of an existing run. + +The service bounds decoded bytes, estimated input tokens, chunks, output records, queued bytes, retries, execution time, and source/checkpoint retention. Inspect the deployment settings and `retentionDeadline`. Idempotency and status retention are bounded; retain your logical document IDs, revisions, ingestion IDs, and recovery decisions in your application. A new revision does not promise automatic removal of every memory from an older revision. + +Extracted records return `documentManaged: true`. They appear in ordinary long-term-memory searches unless you set `excludeDocumentManaged: true`. Their text and attributes are managed by document ingestion: generic overwrite and PATCH requests cannot replace them. Document chunk spans use UTF-8 byte offsets; do not apply those offsets to a character-indexed string. + +With [ingestion webhooks](/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md), submission and generated chunks can also be reviewed before they change data. Check hook decisions separately from ingestion processing. Receiver failure and explicit filtering have different outcomes. + +## Trace memories to their sources + +Provenance connects a memory to document or session inputs used during extraction. A `sourceId` identifies one revisioned origin in the store; an `ingestionId` identifies a particular document run. Session evidence lists input event IDs. Document evidence identifies ingestions, chunks, and optional source spans. These describe the input supplied to extraction, not a guarantee that each sentence in the resulting memory is a quoted fact. + +| Request | Python method | TypeScript method | +|:--------|:--------------|:------------------| +| Memory → sources | `list_memory_source_links_async` | `listMemorySourceLinks` | +| Memory/source → more evidence | `list_memory_source_evidence_async` | `listMemorySourceEvidence` | +| Source → memories for an owner | `list_source_memories_async` | `listSourceMemories` | + +Set `MEMORY_ID` to one returned output ID. These examples follow both source pages and each source's independent evidence pages, then list memories associated with that source for `OWNER_ID`. Add the Python snippet inside the ingestion example's `async with` block, or the TypeScript snippet inside `main`, after output listing. They reuse the same client and request headers. + +{{< multitabs id="document-provenance" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + source_page_token = None + while True: + links = await memory.list_memory_source_links_async( + memory_id=os.environ["MEMORY_ID"], limit=100, + page_token=source_page_token, http_headers=headers, + ) + for source in links.items: + print(source.model_dump_json(by_alias=True)) + evidence_token = source.evidence_next_page_token + while evidence_token: + evidence = await memory.list_memory_source_evidence_async( + memory_id=os.environ["MEMORY_ID"], source_id=source.source_id, + limit=100, page_token=evidence_token, http_headers=headers, + ) + print(evidence.source.model_dump_json(by_alias=True)) + evidence_token = evidence.source.evidence_next_page_token + reverse_token = None + while True: + related = await memory.list_source_memories_async( + source_id=source.source_id, owner_id=os.environ["OWNER_ID"], + limit=100, page_token=reverse_token, http_headers=headers, + ) + print(related.model_dump_json(by_alias=True)) + reverse_token = related.next_page_token + if not reverse_token: + break + source_page_token = links.next_page_token + if not source_page_token: + break +``` + +-tab-sep- + +```typescript + const memoryId = process.env.MEMORY_ID; + if (!memoryId) throw new Error("Set MEMORY_ID to an output memory ID."); + let sourcePageToken: string | undefined; + do { + const links = await memory.listMemorySourceLinks(memoryId, 100, sourcePageToken, options); + for (const source of links.items) { + console.dir(source, { depth: null }); + let evidenceToken = source.evidenceNextPageToken; + while (evidenceToken) { + const evidence = await memory.listMemorySourceEvidence( + memoryId, source.sourceId, 100, evidenceToken, options, + ); + console.dir(evidence.source, { depth: null }); + evidenceToken = evidence.source.evidenceNextPageToken; + } + let reverseToken: string | undefined; + do { + const related = await memory.listSourceMemories( + source.sourceId, OWNER_ID, 100, reverseToken, options, + ); + console.dir(related.items, { depth: null }); + reverseToken = related.nextPageToken; + } while (reverseToken); + } + sourcePageToken = links.nextPageToken; + } while (sourcePageToken); +``` + +-tab-sep- + +```sh +source_token='' +while :; do + links=$(curl --fail-with-body --silent --show-error --get \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: ${USER_TOKEN:-}" \ + --data-urlencode 'limit=100' --data-urlencode "pageToken=$source_token" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/$MEMORY_ID/sources") || exit 1 + printf '%s\n' "$links" | jq .items + printf '%s' "$links" | jq -c '.items[]' | while IFS= read -r source; do + source_id=$(printf '%s' "$source" | jq -r .sourceId) + evidence_token=$(printf '%s' "$source" | jq -r '.evidenceNextPageToken // empty') + while [ -n "$evidence_token" ]; do + evidence=$(curl --fail-with-body --silent --show-error --get \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: ${USER_TOKEN:-}" \ + --data-urlencode 'limit=100' --data-urlencode "pageToken=$evidence_token" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/$MEMORY_ID/sources/$source_id/evidence") || exit 1 + printf '%s\n' "$evidence" | jq .source + evidence_token=$(printf '%s' "$evidence" | jq -r '.source.evidenceNextPageToken // empty') + done + reverse_token='' + while :; do + related=$(curl --fail-with-body --silent --show-error --get \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: ${USER_TOKEN:-}" \ + --data-urlencode "ownerId=$OWNER_ID" --data-urlencode 'limit=100' \ + --data-urlencode "pageToken=$reverse_token" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/sources/$source_id/memories") || exit 1 + printf '%s\n' "$related" | jq .items + reverse_token=$(printf '%s' "$related" | jq -r '.nextPageToken // empty') + [ -n "$reverse_token" ] || break + done + done || exit 1 + source_token=$(printf '%s' "$links" | jq -r '.nextPageToken // empty') + [ -n "$source_token" ] || break +done +``` + +{{< /multitabs >}} + +The evidence response wraps `source`; it does not return a top-level `items` list. The source object contains `evidence` for documents or `sessionEvidence` for sessions. Newer evidence can also include `evidenceId`, `schemaVersion`, `capturedAt`, and `producer`. Handle their absence on legacy data. `capturedAt` is when extraction input was prepared, not when the original event occurred. A retried delivery retains its evidence identity. + +For reverse lookup, use `GET /v1/stores/{storeId}/sources/{sourceId}/memories` with `ownerId`, or the corresponding SDK method. Follow its `nextPageToken` independently. Keep every token with the exact query and endpoint that issued it. Tokens expire; restart a query when one is invalid, and URL-encode tokens in REST calls. + +A source link does not grant access to the original document, session, or another owner's memories. The service checks source and record access separately; sharing an extracted memory does not share the private document it came from. diff --git a/content/develop/ai/context-engine/agent-memory/namespaces/_index.md b/content/develop/ai/context-engine/agent-memory/namespaces/_index.md new file mode 100644 index 0000000000..951f7c610e --- /dev/null +++ b/content/develop/ai/context-engine/agent-memory/namespaces/_index.md @@ -0,0 +1,321 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Group session memory and long-term memory with optional namespaces. +hideListLinks: true +linktitle: Namespaces +title: Namespaces +weight: 13 +--- + +Namespaces are optional. Use them to group memories for a project, team, or user. For example, a travel agent can keep a user's trip memories in a personal `travel` namespace. + +Create the namespace before writing to it. The response contains a server-generated `namespaceId`. Save this ID and use it in `namespaceRef` on session events and long-term memory writes. You will also use this ID to search for memories in that namespace. Names and paths can change when you rename a namespace, but its ID stays the same. + +## Create a personal namespace + +{{< embed-md "agent-memory-continue-from-quickstart.md" >}} + +This example uses a personal namespace to group the trip memories. + +{{< multitabs id="namespace" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +Add this code inside the `with` block. It creates a personal namespace for the user's travel memories when `NAMESPACE_ID` is not set: + +```python + namespace_id = os.environ.get("NAMESPACE_ID") + if not namespace_id: + created_namespace = agent_memory.create_namespace(request={ + "name": "travel", + "scope": models.NamespaceScope.PERSONAL, + "owner_id": USER_ID, + }) + namespace_id = created_namespace.namespace.namespace_id + print(f'export NAMESPACE_ID="{namespace_id}"') + namespace_ref = {"namespace_id": namespace_id} +``` + +Run the file once, then run the printed `export` command in your shell before running the file again. Later runs reuse that ID. Creating the same namespace again returns `409 Conflict`. If you lose the ID, use `list_namespaces` with `scope="PERSONAL"` and `owner_id=USER_ID` to find it. + +Start a new session with this namespace. Extracted memories inherit it: + +```python + agent_memory.add_session_event( + session_id="travel-planning-with-namespace", actor_id=USER_ID, + role=models.MessageRole.USER, + content=[models.Text(text="I am visiting Tokyo and Kyoto next month.")], + created_at=datetime.now(timezone.utc), namespace_ref=namespace_ref, + ) +``` + +-tab-sep- + +Add this code inside `run`. It creates a personal namespace for the user's travel memories when `NAMESPACE_ID` is not set: + +```typescript + let namespaceId = process.env.NAMESPACE_ID; + if (!namespaceId) { + const createdNamespace = await agentMemory.createNamespace({ + name: "travel", + scope: "PERSONAL", + ownerId: userId, + }); + namespaceId = createdNamespace.namespace.namespaceId; + console.log(`export NAMESPACE_ID="${namespaceId}"`); + } + const namespaceRef = { namespaceId }; +``` + +Run the file once, then run the printed `export` command in your shell before running the file again. Later runs reuse that ID. Creating the same namespace again returns `409 Conflict`. If you lose the ID, use `listNamespaces` with `scope: "PERSONAL"` and `ownerId: userId` to find it. + +Start a new session with this namespace. Extracted memories inherit it: + +```typescript + await agentMemory.addSessionEvent({ + sessionId: "travel-planning-with-namespace", actorId: userId, role: "USER", + content: [{ text: "I am visiting Tokyo and Kyoto next month." }], + createdAt: new Date(), namespaceRef, + }); +``` + +-tab-sep- + +Create a personal namespace for the user's travel memories. Run this request once and keep the returned ID in your shell: + +```sh +NAMESPACE_ID=$(curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data "$(jq -n --arg owner "$OWNER_ID" \ + '{name: "travel", scope: "PERSONAL", ownerId: $owner}')" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/namespaces" | jq -er '.namespace.namespaceId') +export NAMESPACE_ID +printf '%s\n' "$NAMESPACE_ID" +``` + +Continue only after the request succeeds and prints a namespace ID. Save that ID for later runs. Creating the same namespace again returns `409 Conflict`; use the existing namespace ID instead. If you lose the ID, list personal roots with `GET /v1/stores/{storeId}/namespaces?scope=PERSONAL&ownerId=`. + +Start a new session with this namespace: + +```sh +curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data "$(jq -n --arg owner "$OWNER_ID" --arg ns "$NAMESPACE_ID" --arg now "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + '{sessionId:"travel-planning-with-namespace",actorId:$owner,role:"USER",content:[{text:"I am visiting Tokyo and Kyoto next month."}],createdAt:$now,namespaceRef:{namespaceId:$ns}}')" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/events" | jq +``` + +{{< /multitabs >}} + +## Create a shared namespace + +A shared namespace groups memories that an application uses across users or agents. For example, a travel-planning team can use a shared `team-travel` namespace for information that everyone on the team needs. The namespace has `SHARED` scope and no namespace owner. Individual long-term memories still have their own `ownerId` values. + +Applications use the same namespace ID when starting sessions or creating long-term memories for the team. Each conversation keeps its own session ID. Memories extracted from those sessions use the shared namespace, so a later search can recall information from several conversations and owners. + +Your application decides who can contribute to and retrieve the team's memories. Check the user's access before writing or searching. `SHARED` scope describes how memories are grouped; it does not grant users permission to access them. + +Use the client and connection values from the [quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}). Create the namespace with `scope` set to `SHARED` and omit `ownerId`: + +{{< multitabs id="shared-namespace" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +Add this code inside the `with` block: + +```python + shared_namespace_id = os.environ.get("SHARED_NAMESPACE_ID") + if not shared_namespace_id: + shared_namespace = agent_memory.create_namespace(request={ + "name": "team-travel", + "scope": models.NamespaceScope.SHARED, + }) + shared_namespace_id = shared_namespace.namespace.namespace_id + print(f'export SHARED_NAMESPACE_ID="{shared_namespace_id}"') + shared_namespace_ref = {"namespace_id": shared_namespace_id} +``` + +Run the file once, then run the printed `export` command in your shell. Later runs reuse the namespace ID. + +-tab-sep- + +Add this code inside `run`: + +```typescript + let sharedNamespaceId = process.env.SHARED_NAMESPACE_ID; + if (!sharedNamespaceId) { + const sharedNamespace = await agentMemory.createNamespace({ + name: "team-travel", + scope: "SHARED", + }); + sharedNamespaceId = sharedNamespace.namespace.namespaceId; + console.log(`export SHARED_NAMESPACE_ID="${sharedNamespaceId}"`); + } + const sharedNamespaceRef = { namespaceId: sharedNamespaceId }; +``` + +Run the file once, then run the printed `export` command in your shell. Later runs reuse the namespace ID. + +-tab-sep- + +Run this request once and save the returned ID: + +```sh +SHARED_NAMESPACE_ID=$(curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data '{"name":"team-travel","scope":"SHARED"}' \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/namespaces" | jq -er '.namespace.namespaceId') +export SHARED_NAMESPACE_ID +printf '%s\n' "$SHARED_NAMESPACE_ID" +``` + +Continue after the request succeeds and prints a namespace ID. Reuse that ID for later requests. + +{{< /multitabs >}} + +Creating the same root again returns `409 Conflict`. If you lose the ID, list root namespaces with `scope=SHARED` and find `team-travel`. + +To add a conversation to this namespace, use a new session ID and set its first event's namespace reference to the shared ID. Use `namespace_ref=shared_namespace_ref` in Python, `namespaceRef: sharedNamespaceRef` in TypeScript, or `"namespaceRef": {"namespaceId": "$SHARED_NAMESPACE_ID"}` in curl. For direct long-term memory creation, set the reference on each record. Creating the namespace does not move existing memories into it. + +### Recall shared memories + +After writing memories to the shared namespace, search with its namespace ID. The examples omit the owner filter so results can include memories associated with different owners in that namespace. Add an `ownerId` filter when you need only one owner's memories. An empty namespace returns no memories. + +{{< multitabs id="shared-memory-search" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + shared_results = agent_memory.search_long_term_memory(request={ + "text": "What travel requirements has the team discussed?", + "filter_": {"namespace_ref": {"eq": shared_namespace_id}}, + "limit": 5, + }) + show("Shared travel memories", shared_results) +``` + +-tab-sep- + +```typescript + const sharedResults = await agentMemory.searchLongTermMemory({ + text: "What travel requirements has the team discussed?", + filter: { namespaceRef: { eq: sharedNamespaceId } }, + limit: 5, + }); + console.dir(sharedResults, { depth: null }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data "$(jq -n --arg ns "$SHARED_NAMESPACE_ID" \ + '{text: "What travel requirements has the team discussed?", filter: {namespaceRef: {eq: $ns}}, limit: 5}')" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/search" | jq +``` + +{{< /multitabs >}} + +The namespace filter selects that exact namespace. To search its children too, list them and include their IDs with `filter.namespaceRef.in`. + +## Place and retrieve memories + +Pass `namespaceRef: {"namespaceId": ""}` when you start a session. The first event fixes the session namespace. Later events cannot add or change it. Memories extracted from the session use that namespace, so use a new session ID when adding a namespace to the quickstart example. + +For direct long-term memory creation, set `namespaceRef` on each record. To move existing records, use `move_long_term_memories` (Python), `moveLongTermMemories` (TypeScript), or `POST /v1/stores/{storeId}/long-term-memory/move` with their IDs and the destination `namespaceRef`. Check both `moved` and `errors` in the response before treating a batch as complete. + +Search with `filter.namespaceRef.eq` for one namespace or `filter.namespaceRef.in` for several IDs. These filters match exact namespace IDs; they do not expand a parent into its descendants. List the children and include their IDs when you need to search several levels of a hierarchy. Keep the `ownerId` filter when recalling one user's memories. + +Your application must check that the current user can access each namespace before querying it. In on-prem deployments with ownership enforcement enabled, personal namespace access follows the root owner's authenticated user ID. Shared namespaces are accessible to callers with the required store permission. If a shared namespace is only for one team, enforce that rule in your application or use separate stores and grants. See [Authorize every memory request](/content/develop/ai/context-engine/agent-memory/developer-guide.md#authorize-every-memory-request). + +## Create a namespace hierarchy + +Choose a root scope when you create a namespace: + +| Scope | Creation fields | Example use | +|:------|:----------------|:------------| +| `PERSONAL` | `name`, `scope`, and `ownerId` | A user's travel plans. | +| `SHARED` | `name` and `scope`, without `ownerId` | Information used across a team. | + +Create a child with `name` and `parentId`. You can also create a hierarchy with `path`, such as `travel/japan`, and the root scope. Supply `ownerId` for a personal path. Missing path segments are created as needed. Do not combine `path` with `name` or `parentId`. + +To create a child, pass this request body to `create_namespace` (Python), `createNamespace` (TypeScript), or `POST /v1/stores/{storeId}/namespaces`: + +```json +{"name": "japan", "parentId": ""} +``` + +To create a personal path and any missing segments: + +```json +{"path": "travel/japan", "scope": "PERSONAL", "ownerId": ""} +``` + +Names and paths are case-sensitive. The path does not encode scope. Use the returned ID to distinguish namespaces that have the same path under different owners or scopes. + +For runnable examples, see [Create a personal namespace]({{< relref "/develop/ai/context-engine/agent-memory/namespaces#create-a-personal-namespace" >}}) or [Create a shared namespace]({{< relref "/develop/ai/context-engine/agent-memory/namespaces#create-a-shared-namespace" >}}). + +## Manage namespaces + +| Operation | Behavior | +|:----------|:---------| +| List | List roots, or pass `parentId` to list direct children. Sort by `id`, `name`, `createdAt`, or `updatedAt` with `sortBy`; `sortOrder` accepts `asc` or `desc`. Defaults are `id` and `asc`. Follow `nextPageToken` with the same filters and sort options. | +| Get | Retrieve the current name, path, scope, and state by ID. | +| Rename | Update `name`; continue using the same ID for placement and retrieval. | +| Archive | Set `state` to `ARCHIVED` to stop new children and placements. You cannot unarchive a namespace. | +| Delete | Delete an empty leaf namespace. A namespace with children or memory placements returns `409 Conflict`. | + +Creating a namespace at an occupied location returns `409 Conflict`, including when the existing namespace is archived. Save and reuse the returned ID instead of creating the same namespace on each agent turn. For request fields and error responses, see the [namespace API reference]({{< relref "/develop/ai/context-engine/agent-memory/api-reference" >}}#operation/CreateNamespace). + +## Sort namespaces + +Use the client from the [quickstart](/content/develop/ai/context-engine/agent-memory/quickstart.md). This example lists shared roots by name: + +{{< multitabs id="namespace-sort" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + namespaces = agent_memory.list_namespaces( + scope="SHARED", limit=20, sort_by="name", sort_order="asc", + ) + show("Shared namespaces", namespaces) +``` + +-tab-sep- + +```typescript + const namespaces = await agentMemory.listNamespaces( + undefined, // parentId + "SHARED", // scope + undefined, // ownerId + 20, // limit + undefined, // pageToken + undefined, // request options + "name", // sortBy + "asc", // sortOrder + ); + console.dir(namespaces, { depth: null }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error --get \ + --header "Authorization: Bearer $API_KEY" \ + --data-urlencode 'scope=SHARED' --data-urlencode 'limit=20' \ + --data-urlencode 'sortBy=name' --data-urlencode 'sortOrder=asc' \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/namespaces" | jq +``` + +{{< /multitabs >}} + +Add the Python snippet inside the `with` block and the TypeScript snippet inside `run`. Name sorting uses the display name with Redis text collation, rather than the hierarchy path. Equal sort values use namespace ID ascending as a tie-breaker. + +## Route and admit shared memories + +An explicit namespace destination must pass its admission rules. Direct creation without a namespace stays unassigned. To route an existing shared memory, omit `namespaceRef` from a move request. Personal and unassigned memories are not candidates for automatic routing. See [Namespace routing](/content/develop/ai/context-engine/agent-memory/namespaces/namespace-routing.md) for policy setup, examples, and per-item errors. diff --git a/content/develop/ai/context-engine/agent-memory/namespaces/namespace-routing.md b/content/develop/ai/context-engine/agent-memory/namespaces/namespace-routing.md new file mode 100644 index 0000000000..8181664617 --- /dev/null +++ b/content/develop/ai/context-engine/agent-memory/namespaces/namespace-routing.md @@ -0,0 +1,192 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Configure shared namespace routing and admission rules for long-term memories. +hideListLinks: true +linktitle: Namespace routing +title: Namespace routing +weight: 14 +aliases: +- /develop/ai/context-engine/agent-memory/namespace-routing/ +--- + +Namespace routing selects a shared namespace for an eligible memory. Admission rules check whether a memory can enter a destination. Use routing to organize shared memories by content, and admission rules to reject content that does not belong in a namespace. + +The policy applies to one store. It targets existing shared namespace resources, using their stable `namespaceId` values. It does not create namespaces or change access permissions. Namespace routing is available in Redis Cloud and enabled in self-managed deployments. Configure a policy to use it for a store. For self-managed process settings, see [Configure namespace routing](/content/operate/iris/agent-memory/self-managed/configuration.md#enable-namespace-routing). + +## Understand placement and admission + +| Request | Behavior | +|:--------|:---------| +| Create a memory with `namespaceRef` | Use that destination after admission checks. The service never chooses another destination. | +| Create a memory without `namespace` or `namespaceRef` | Create it without a namespace. Automatic routing does not place it in a shared namespace. | +| Move a memory with `namespaceRef` | Use that destination after admission checks. | +| Move a memory without `namespaceRef` | Evaluate automatic routing only if the memory is already in a shared namespace. | +| Update a memory | Check the merged record against admission at its current destination. An update never chooses another destination. | + +An automatic move keeps the current placement and returns a per-item error if routing is disabled, no destination qualifies, or the memory is personal or has no namespace. Inspect both `moved` and `errors` in a move response. Bulk creation also reports failures per record in `errors`. An explicit move to an archived namespace returns `409 Conflict`. + +## Configure a policy + +1. [Create a shared namespace](/content/develop/ai/context-engine/agent-memory/namespaces/_index.md#create-a-shared-namespace) and save its ID as `SHARED_NAMESPACE_ID`. +1. Install the software development kit (SDK) for your language. +1. Set `AGENT_MEMORY_URL`, `STORE_ID`, and `API_KEY` as described in the [developer guide](/content/develop/ai/context-engine/agent-memory/developer-guide.md#connect-to-a-redis-agent-memory-service). + +This example accepts text containing `travel` into that namespace, ignoring case. **PUT replaces the entire policy.** Include any existing rules you want to keep in `rules`; the example installs only the `travel` rule. + +{{< multitabs id="namespace-routing-policy" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python +import asyncio +import os + +from redis_agent_memory import AgentMemory + + +async def main(): + async with AgentMemory( + os.environ["AGENT_MEMORY_URL"], + store_id=os.environ["STORE_ID"], + api_key=os.environ["API_KEY"], + ) as agent_memory: + current = await agent_memory.get_namespace_routing_async() + print(current.model_dump_json(by_alias=True, indent=2)) + result = await agent_memory.put_namespace_routing_async(policy={ + "enabled": True, + "rules": [{ + "id": "travel", + "enabled": True, + "action": "accept", + "destination": {"namespace_id": os.environ["SHARED_NAMESPACE_ID"]}, + "strategies": [{ + "kind": "matcher_spec", + "matcher": {"kind": "regex", "regex": {"pattern": "(?i)travel"}}, + }], + }], + }) + print(result.model_dump_json(by_alias=True, indent=2)) + + +asyncio.run(main()) +``` + +-tab-sep- + +```typescript +import { AgentMemory } from "@redis-iris/agent-memory"; + +async function main() { + const { AGENT_MEMORY_URL, STORE_ID, API_KEY, SHARED_NAMESPACE_ID } = process.env; + if (!AGENT_MEMORY_URL || !STORE_ID || !API_KEY || !SHARED_NAMESPACE_ID) { + throw new Error("Set AGENT_MEMORY_URL, STORE_ID, API_KEY, and SHARED_NAMESPACE_ID."); + } + const agentMemory = new AgentMemory({ + serverURL: AGENT_MEMORY_URL, storeId: STORE_ID, apiKey: API_KEY, + }); + console.dir(await agentMemory.getNamespaceRouting(), { depth: null }); + const result = await agentMemory.putNamespaceRouting({ + policy: { + enabled: true, + rules: [{ + id: "travel", enabled: true, action: "accept", + destination: { namespaceId: SHARED_NAMESPACE_ID }, + strategies: [{ + kind: "matcher_spec", + matcher: { kind: "regex", regex: { pattern: "(?i)travel" } }, + }], + }], + }, + }); + console.dir(result, { depth: null }); +} + +main().catch((error) => { console.error(error instanceof Error ? error.message : "Request failed"); process.exitCode = 1; }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $API_KEY" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/namespace-routing" | jq + +curl --fail-with-body --silent --show-error \ + --request PUT \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data "$(jq -n --arg ns "$SHARED_NAMESPACE_ID" \ + '{policy: {enabled: true, rules: [{id: "travel", enabled: true, + action: "accept", destination: {namespaceId: $ns}, strategies: [{ + kind: "matcher_spec", matcher: {kind: "regex", regex: {pattern: "(?i)travel"}} + }]}]}}')" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/namespace-routing" | jq +``` + +{{< /multitabs >}} + +## How rules are evaluated + +In this example, `travel` is an accept rule with one strategy, a regex matcher. + +An enabled `accept` rule selects a candidate destination. Accept rules run in list order. Every enabled `reject` rule for that destination applies, including rules later in the list. A matching reject vetoes the candidate. Automatic routing then tries another candidate; explicit placement fails rather than choosing another namespace. + +Strategies within a rule run in list order; the first match fires the rule. An accept rule with no strategies always matches. A reject rule with no strategies never matches. Use `matcher_spec` with a `regex` matcher for text patterns, or `llm_prompt` with `llmPrompt` for a model-evaluated instruction. Each strategy must contain only the block for its declared `kind`. + +A store without a saved policy returns an empty, disabled policy. Regular expressions use RE2 syntax. Lookaround, backreferences, invalid expressions, and patterns that can match empty text are not supported. A policy supports up to 32 rules and 8 strategies per rule. See [`PutNamespaceRouting`](/content/develop/ai/context-engine/agent-memory/api-reference.md#operation/PutNamespaceRouting) for validation and error responses. + +## Move memories with the policy + +To evaluate the policy for an existing shared memory, omit the destination from the move request. First create a test memory in another shared namespace. Run this setup once, after setting `OWNER_ID` to your application user: + +```sh +SOURCE_NAMESPACE_ID=$(curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data '{"name":"routing-source","scope":"SHARED"}' \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/namespaces" | jq -er '.namespace.namespaceId') || exit 1 +export MEMORY_ID='routing-travel-example' +curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data "$(jq -n --arg id "$MEMORY_ID" --arg owner "$OWNER_ID" --arg ns "$SOURCE_NAMESPACE_ID" \ + '{memories:[{id:$id,ownerId:$owner,text:"Travel policy allows train tickets.",namespaceRef:{namespaceId:$ns}}]}')" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory" | jq +``` + +Continue only if the namespace request succeeds and the memory response has no per-record errors. Run the move in the same shell, or pass the exported `MEMORY_ID` to your SDK process: + +{{< multitabs id="namespace-routing-move" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + moved = await agent_memory.move_long_term_memories_async( + memory_ids=[os.environ["MEMORY_ID"]], + ) + print(moved.model_dump_json(by_alias=True, indent=2)) +``` + +-tab-sep- + +```typescript + const memoryId = process.env.MEMORY_ID; + if (!memoryId) throw new Error("Set MEMORY_ID to an existing shared memory ID."); + const moved = await agentMemory.moveLongTermMemories({ memoryIds: [memoryId] }); + console.dir(moved, { depth: null }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data "$(jq -n --arg id "$MEMORY_ID" '{memoryIds: [$id]}')" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/move" | jq +``` + +{{< /multitabs >}} + +Add the SDK snippet inside the client block or `main` function from [Configure a policy](/content/develop/ai/context-engine/agent-memory/namespaces/namespace-routing.md#configure-a-policy). Set `MEMORY_ID` before running it. The example memory must already belong to a shared namespace and contain matching text. The request accepts 1–100 IDs. To choose a destination explicitly, also pass `namespace_ref={"namespace_id": ""}` in Python or `namespaceRef: { namespaceId: "" }` in TypeScript or the request body. Explicit placement still runs admission checks. diff --git a/content/develop/ai/context-engine/agent-memory/overview.md b/content/develop/ai/context-engine/agent-memory/overview.md index 442f14cf4f..6caf531cbd 100644 --- a/content/develop/ai/context-engine/agent-memory/overview.md +++ b/content/develop/ai/context-engine/agent-memory/overview.md @@ -15,15 +15,15 @@ aliases: ## Background summarization and extraction -Agent Memory has two writers. Your application writes session events as they happen. After you enable summarization and extraction, Agent Memory reads those events in the background and does two things without an application write: +Your application writes session events as they happen. In the background, Redis Agent Memory reads those events and: -- **Summarizes** older events into a compact summary once the session passes a configured threshold, so a long conversation doesn't blow the model's context window. +- **Summarizes** older events into a compact summary once the session passes a configured threshold, so a long conversation doesn't overflow the model's context window. - **Extracts** long-term memories, facts and preferences worth keeping, and writes them as separate, searchable records with vector embeddings. ![An application writes session events synchronously to session memory. Background extraction moves durable information into long-term memory, which the application can search directly.](/images/ai/context-engine/agent-memory-writers.svg) {width="700"} -Both run asynchronously, to keep session writes fast. Extraction also weighs a new memory against existing memories before writing it. Rather than rejecting anything that looks similar, it uses model judgment to decide whether a near-identical memory is a true duplicate or is meaningfully different and worth keeping too. +Both run asynchronously to keep session writes fast. Extraction also compares each new memory with existing ones and uses the model to decide whether it duplicates existing information or adds something new. ## Memory types @@ -37,18 +37,18 @@ You can think of Redis Agent Memory's tiers as analogous to human memory: Session memory is the working set: the current conversation's events, read and written every turn. Long-term memory is what survives after the session ends, durable enough to recall in a conversation the agent hasn't seen before. -This table is a simplified starting point, not the full picture. Redis Agent Memory also maintains an automatically updated summary of each session, as derived session state. It returns this summary separately from long-term memories. You can also define custom long-term memory types for domain-specific information. See [Memory types & extraction]({{< relref "/operate/iris/agent-memory/create-service#memory-types-and-extraction" >}}) for the complete set of built-in and custom types. +Redis Agent Memory also maintains an automatically updated summary of each session, as derived session state. It returns this summary separately from long-term memories. You can also define custom long-term memory types for domain-specific information. See [Memory types & extraction]({{< relref "/operate/iris/agent-memory/create-service#memory-types-and-extraction" >}}) for the complete set of built-in and custom types. ## Adding to session memory -Your application gives Agent Memory the conversation for extracting long-term memories by appending to session memory. Think of session memory as a shared transcript that both your application and the background extraction pipeline read. +Your application appends events to session memory. Both your application and background extraction read this conversation transcript. ### Key terms - **Session event**: One message in a conversation, added with `sessionId`, `actorId`, `role`, `content`, and `createdAt`. - **actorId**: Identifies who or what sent the event, a user, the agent, or a tool. - **ownerId**: Identifies who a long-term memory belongs to, for search and filtering later. Independent of who wrote the session event it came from. -- **memoryType**: The long-term memory tier a fact is stored under, one of the built-in types (`semantic`, `episodic`, `message`, `session_summary_view`) or a custom type you define. +- **memoryType**: The category of a long-term memory, such as `semantic`, `episodic`, or a custom type you define. ### How it works @@ -56,18 +56,26 @@ Your application gives Agent Memory the conversation for extracting long-term me 2. **Extraction reads the session.** In the background, extraction reads the session's accumulated events, not just the latest one, so it can resolve context across turns. 3. **Long-term memory is written.** Facts worth keeping are stored as separate, searchable records, scoped by owner and memory type. -Extraction reads the whole session. You only need to send each new turn as it happens. You don't need to resend earlier turns for a later one to be understood in context. +Send each new turn once. Extraction reads earlier events from the session, so you don't need to resend them. + +## Namespaces + +Namespaces group related conversations and long-term memories. For example, a travel application can keep a user's trip plans in a personal `travel` namespace. A shared namespace can hold information that several users need, such as a team's travel policy. + +Each namespace has a stable ID. Your application uses that ID when it stores or retrieves memories, even if the namespace's name changes. Namespaces are optional; you can use session and long-term memory without creating one. + +See [Namespaces]({{< relref "/develop/ai/context-engine/agent-memory/namespaces" >}}) for examples. To store conversation events or recall information across conversations, see [Sessions]({{< relref "/develop/ai/context-engine/agent-memory/sessions" >}}) and [Long-term memory]({{< relref "/develop/ai/context-engine/agent-memory/long-term-memory" >}}). ## FAQ **Do I still need a separate store for conversation state?** -No, not for that purpose. Session memory holds the ordered events of a conversation, with automatic expiration based on a time-to-live (TTL) you set, and no schema to design. It doesn't replace a general-purpose cache or your application's other session data unrelated to the conversation. +No. Session memory stores the ordered events of a conversation, with automatic expiration based on a time-to-live (TTL) you set, and no schema to design. It doesn't replace a general-purpose cache or your application's other session data unrelated to the conversation. **What happens if I write directly to long-term memory instead of letting extraction do it?** Both paths are supported. Use direct writes for bulk imports or external knowledge sources: anything that didn't originate in a conversation. Automatic extraction is for facts that emerge from session events themselves. ## Next steps -- Try the [Agent Memory interactive demo](content/develop/ai/context-engine/agent-memory/interactive-demo.md) to watch session memory and long-term memory change during a conversation. +- Try the [Agent Memory interactive demo]({{< relref "/develop/ai/context-engine/agent-memory/interactive-demo" >}}) to watch session memory and long-term memory change during a conversation. - [Developer guide]({{< relref "/develop/ai/context-engine/agent-memory/developer-guide" >}}) to connect an application and start writing session events. -- [Python SDK quickstart]({{< relref "/develop/ai/context-engine/agent-memory/python-sdk-quickstart" >}}), [TypeScript SDK quickstart]({{< relref "/develop/ai/context-engine/agent-memory/typescript-sdk-quickstart" >}}), or [REST API quickstart]({{< relref "/develop/ai/context-engine/agent-memory/rest-api-quickstart" >}}) to see session memory, extraction, and summarization in action. +- [Quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}) to store a conversation and search extracted memories with Python, TypeScript, or curl. Then [try automatic summarization]({{< relref "/develop/ai/context-engine/agent-memory/sessions#try-automatic-summarization" >}}) to see how older messages become a summary. diff --git a/content/develop/ai/context-engine/agent-memory/python-sdk-quickstart.md b/content/develop/ai/context-engine/agent-memory/python-sdk-quickstart.md deleted file mode 100644 index 27e8a9c459..0000000000 --- a/content/develop/ai/context-engine/agent-memory/python-sdk-quickstart.md +++ /dev/null @@ -1,279 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- develop -- ai -description: Explore session memory, automatic extraction, summarization, custom memory types, and sensitive-data exclusions with the Redis Agent Memory Python SDK. -hideListLinks: true -linktitle: Python SDK quickstart -title: Redis Agent Memory Python SDK quickstart -weight: 6 ---- - -Use this quickstart to follow a travel planning conversation through Redis Agent Memory. You will retrieve the conversation from session memory, recall information extracted in the background, inspect an automatically generated session summary, extract structured travel information, and guide extraction away from sensitive data. - -## Before you begin - -To complete this quickstart, you need: - -{{< embed-md "rc-agent-memory-quickstart-prerequisites.md" >}} - -You also need Python 3.10 or later. - -## Create a Redis Agent Memory service - -{{< embed-md "rc-agent-memory-quickstart-create-service.md" >}} - -## Save the connection values - -1. Open the Redis Agent Memory service in the Redis Cloud console. -1. On the **Configuration** tab, copy the **Endpoint** and **Store ID**. -1. Export the API key in your shell: - - ```sh - export API_KEY='' - ``` - -Keep the API key out of source control, application logs, and other unsecured locations. - -## Install the SDK - -```sh -python -m pip install redis-agent-memory -``` - -## Create the client and check the service health - -Create `quickstart.py` with the following code. Replace `` and `` with the values from Redis Cloud. The endpoint must include `https://`. - -```python -import os -from datetime import datetime, timezone - -from redis_agent_memory import AgentMemory, models - - -ENDPOINT = "" -STORE_ID = "" -SESSION_ID = "travel-planning-session" -USER_ID = "quickstart-user" - - -def show(label, response): - print(f"{label}:") - print(response.model_dump_json(by_alias=True, indent=2)) - - -def main(): - with AgentMemory( - ENDPOINT, - store_id=STORE_ID, - api_key=os.environ["API_KEY"], - ) as agent_memory: - health = agent_memory.health() - show("Service health", health) - - -if __name__ == "__main__": - main() -``` - -Run the file: - -```sh -python quickstart.py -``` - -A healthy response confirms that the client can reach Redis Agent Memory and authenticate with the API key. The first store request validates the Store ID. - -## 1. Build conversation context with session memory - -Session memory stores a conversation as an ordered sequence of events. Add the following code after the health check, inside the `with` block: - -```python - event = agent_memory.add_session_event( - session_id=SESSION_ID, - actor_id=USER_ID, - role=models.MessageRole.USER, - content=[models.Text( - text=( - "I am visiting Tokyo and Kyoto next month. " - "I am vegetarian and prefer spicy food." - ), - )], - created_at=datetime.now(timezone.utc), - ) - show("Created event", event) - - session = agent_memory.get_session_memory( - session_id=SESSION_ID, - ) - show("Session memory", session) -``` - -Run the file again. The session response contains the stored message, its role, actor, and timestamps. An application can retrieve this session before the next agent turn and add the events to the model's context. - -> [!NOTE] -> **What to expect:** The `events` array contains the travel message. Redis Agent Memory adds an `eventId` and `systemTimestamp`, showing that the application can recover the complete event later using only the session ID. - -After the event is stored, comment out the call to `add_session_event` before subsequent runs to avoid adding the same message again. - -## 2. Recall automatically extracted information - -Redis Agent Memory processes session events in the background and creates long term memories for information that may be useful in later conversations. You configured the extraction cadence to one minute when you created the service. You do not need to call a memory creation method. - -Wait at least one minute, then add this search after the session retrieval: - -```python - results = agent_memory.search_long_term_memory( - request={ - "text": "What dietary requirements and food preferences does the user have?", - "filter_": { - "owner_id": { - "eq": USER_ID, - } - }, - "limit": 5, - }, - ) - show("Automatically extracted memories", results) -``` - -Run the file. The `items` array should contain memories derived from the conversation, such as the vegetarian requirement or preference for spicy food. Extraction is asynchronous, so run the search again if the array is empty. - -> [!NOTE] -> **What to expect:** Results similar to `User is a vegetarian` and `User prefers spicy food`. Your application did not create these memories directly. Redis Agent Memory derived them from the session event. The exact text and memory types can vary. - -The extracted memory remains searchable after the session expires, subject to the long term memory TTL. You can change the extraction cadence and both TTLs in the [Redis Agent Memory service configuration](/content/operate/iris/agent-memory/create-service.md#memory-configuration). - -The Python SDK uses snake case for method arguments and request fields. Serialized API requests and responses use camel case. - -## 3. Keep long conversations concise with automatic summarization - -Automatic summarization condenses older events and retains the most recent events in full. The retrieved session then contains a `summary` object and the recent `events` array, so the application can provide useful history without filling the model's context window with every original message. - -You enabled automatic summarization when you created the service. When the session reaches six events, Redis Agent Memory summarizes the older events and retains the two most recent events in full. - -### Add conversation turns - -Add this code after the first session event to continue the conversation past the configured threshold: - -```python - turns = [ - (models.MessageRole.ASSISTANT, "What dates are you traveling?"), - (models.MessageRole.USER, "I arrive on October 10 and leave on October 18."), - (models.MessageRole.ASSISTANT, "Would you like formal or casual restaurants?"), - (models.MessageRole.USER, "Mostly casual places near public transit."), - (models.MessageRole.ASSISTANT, "Do you have a preferred budget?"), - (models.MessageRole.USER, "About 40 euros per person."), - ] - - for role, text in turns: - agent_memory.add_session_event( - session_id=SESSION_ID, - actor_id=USER_ID if role == models.MessageRole.USER else "travel-agent", - role=role, - content=[models.Text(text=text)], - created_at=datetime.now(timezone.utc), - ) -``` - -Run the code once, then comment out the loop to avoid adding the same turns again. Summarization runs in the background. - -### Retrieve the summarized session - -After a short wait, retrieve the session again: - -```python - compacted_session = agent_memory.get_session_memory( - session_id=SESSION_ID, - ) - show("Compacted session memory", compacted_session) -``` - -Run the retrieval again after a short wait if `summary` is not present. Compare `summary.text` with the recent events. The summary should preserve earlier trip decisions while recent turns remain available in full. - -> [!NOTE] -> **What to expect:** A `summary` object that preserves details such as Tokyo, Kyoto, the travel dates, and food preferences. `summarizedUpToEventId` identifies the last event covered by the summary, while `events` contains the newer turns that remain in full. The exact summary text can vary. - -See [automatic summarization configuration](/content/operate/iris/agent-memory/create-service.md#automatic-summarization) for details. - -## 4. Extract business specific data with a custom memory type - -Built in memories preserve generally useful information. Custom memory types let an application extract structured information for its business domain. You configured `trip_preference` when you created the service, so it processed the same travel planning event independently. - -Search for the structured memory: - -```python - custom_results = agent_memory.search_long_term_memory( - request={ - "text": "What are the requirements for the user's trip?", - "filter_": { - "owner_id": {"eq": USER_ID}, - "memory_type": {"eq": "trip_preference"}, - }, - "limit": 5, - }, - ) - show("Trip preference memories", custom_results) -``` - -The result uses `trip_preference` as its `memoryType` and contains travel information extracted from the conversation. The exact text and returned fields depend on the conversation, extraction model, and client. - -> [!NOTE] -> **What to expect:** A result with `memoryType` set to `trip_preference` that combines the destinations, travel period, and dietary preferences. This shows that the custom type processed the same conversation independently from the built-in memory types. - -See [custom memory types](/content/operate/iris/agent-memory/create-service.md#custom-memory-types) for configuration requirements and limits. - -## 5. Guide extraction away from sensitive data - -The semantic exclusion prompt tells Redis Agent Memory which information should not be kept in long-term memory. Add an event containing a fictional booking code and information that is safe to retain: - -```python - sensitive_event = agent_memory.add_session_event( - session_id=SESSION_ID, - actor_id=USER_ID, - role=models.MessageRole.USER, - content=[models.Text( - text=( - "I booked Hotel Sakura in Tokyo. For this example, " - "the fictional booking confirmation code is DEMO-7QX9." - ), - )], - created_at=datetime.now(timezone.utc), - ) - show("Event with excluded information", sensitive_event) -``` - -Run the code once, then comment out the call to `add_session_event`. Wait at least one minute and search for the safe hotel information: - -```python - exclusion_results = agent_memory.search_long_term_memory( - request={ - "text": "Where is the user staying in Tokyo?", - "filter_": { - "owner_id": {"eq": USER_ID}, - }, - "limit": 5, - }, - ) - show("Memories after semantic exclusion", exclusion_results) -``` - -Inspect the returned memories. They can retain the hotel name, but should not contain `DEMO-7QX9` because the exclusion prompt covers booking confirmation codes. - -> [!NOTE] -> **What to expect:** A memory similar to `User booked Hotel Sakura in Tokyo` without the fictional confirmation code. If the code appears, refine the exclusion prompt and test again. Exclusions remain advisory. - -> [!WARNING] -> Semantic exclusions are advisory and do not guarantee that sensitive information is excluded. Session content still reaches the extraction model provider. Do not use real sensitive data in this exercise. Exclusions do not apply to directly created long-term memories. - -See [sensitive-data exclusions](/content/operate/iris/agent-memory/create-service.md#sensitive-data-exclusions) for configuration details. - -## Next steps - -* Review the [Python SDK package and reference](https://pypi.org/project/redis-agent-memory/). -* Generate a complete conversational agent with the [AI agent builder](/content/develop/ai/agent-builder/_index.md). Select **Redis Iris Conversational Assistant** and **Python**. -* Try the [TypeScript SDK quickstart](/content/develop/ai/context-engine/agent-memory/typescript-sdk-quickstart.md) or [REST API quickstart](/content/develop/ai/context-engine/agent-memory/rest-api-quickstart.md). -* Learn when to [create long term memories directly](/content/develop/ai/context-engine/agent-memory/developer-guide.md#create-long-term-memories). diff --git a/content/develop/ai/context-engine/agent-memory/quickstart.md b/content/develop/ai/context-engine/agent-memory/quickstart.md new file mode 100644 index 0000000000..8d75a4be43 --- /dev/null +++ b/content/develop/ai/context-engine/agent-memory/quickstart.md @@ -0,0 +1,237 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Store and retrieve a conversation, then recall long-term memory with Python, TypeScript, or curl. +hideListLinks: true +linktitle: Quickstart +title: Redis Agent Memory quickstart +weight: 5 +aliases: +- /develop/ai/context-engine/agent-memory/sdk-quickstart/ +- /develop/ai/context-engine/agent-memory/python-sdk-quickstart/ +- /develop/ai/context-engine/agent-memory/typescript-sdk-quickstart/ +- /develop/ai/context-engine/agent-memory/rest-api-quickstart/ +- /develop/ai/context-engine/agent-memory/api-examples/ +- /operate/rc/context-engine/agent-memory/use-agent-memory/ +--- + +Build the memory for a travel assistant: save a conversation, retrieve it, and recall the user’s food preferences in a later conversation. + +## Before you begin + +To complete this quickstart, you need: + +{{< embed-md "rc-agent-memory-quickstart-prerequisites.md" >}} + +Choose Python, TypeScript, or curl for the examples and use it throughout this page. + +{{< multitabs id="prerequisites" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +You also need Python 3.10 or later. + +-tab-sep- + +You also need Node.js and npm. + +-tab-sep- + +You also need a shell with `curl` and `jq` installed. + +{{< /multitabs >}} + +## Create a Redis Agent Memory service + +{{< embed-md "rc-agent-memory-quickstart-create-service.md" >}} + +## Save the connection values + +1. Open the Redis Agent Memory service in the Redis Cloud console. +1. On the **Configuration** tab, copy the **Endpoint** and **Store ID**. +1. Export the values for your chosen client in your shell: + +{{< multitabs id="connection-values" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```sh +export API_KEY='' +``` + +-tab-sep- + +```sh +export API_KEY='' +``` + +-tab-sep- + +```sh +export AGENT_MEMORY_URL='' +export STORE_ID='' +export API_KEY='' +export SESSION_ID='travel-planning-session' +export OWNER_ID='quickstart-user' +``` + +{{< /multitabs >}} + +The SDK examples read `API_KEY` from your environment; replace `` and `` in each file. The curl examples read all three values from your environment. + +The endpoint must include `https://`. Keep the API key out of source control, application logs, and other unsecured locations. + +For a self-managed service with ownership enforcement, use the authenticated client setup in [Delegate a user with an agent key](/content/operate/iris/agent-memory/self-managed/authentication.md#delegate-a-user-with-an-agent-key) and set `OWNER_ID` to the verified user ID before using these examples. + +## Install the SDK + +{{< multitabs id="installation" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +Create and activate a virtual environment with [uv](https://docs.astral.sh/uv/): + +```sh +uv venv +source .venv/bin/activate +uv pip install redis-agent-memory +``` + +-tab-sep- + +```sh +mkdir agent-memory-quickstart +cd agent-memory-quickstart +npm init -y +npm install @redis-iris/agent-memory +npm install --save-dev tsx +``` + +-tab-sep- + +Use `curl` and `jq` directly. No SDK installation is required. + +{{< /multitabs >}} + + + +## Create the client and check the service health + +1. Create `quickstart.py` or `quickstart.ts` with the complete example for your language. +1. Replace `` and `` with your service values. The endpoint must include `https://`. +1. Run `python quickstart.py` or `npx tsx quickstart.ts`. For curl, run the command in your configured shell. +1. Check that the overall `status` and each entry under `features` are healthy. + +{{< clients-example set="agent_memory_sdk" step="connect_health_check" footer="hide" dft_tab_name="curl" runnable="false" try_it="false" >}} +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $API_KEY" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/health" | jq +{{< /clients-example >}} + +The health response reports the status of the selected store's memory features. A healthy response looks like: + +```json +{ + "status": "healthy", + "features": { + "background_jobs": { + "status": "healthy" + }, + "long_term_memory": { + "status": "healthy" + }, + "session_memory": { + "status": "healthy" + }, + "source_link": { + "status": "healthy" + }, + "store_db": { + "status": "healthy" + } + } +} +``` + +Each SDK example includes its client setup. Replace the file at each step, keeping the same connection values and identifiers. Run curl commands in the shell where you exported the connection values. + + + +## Save and retrieve a conversation + +Session memory is the conversation transcript. Each message is an event, stored with the conversation's session ID and the sender's actor ID. The first request saves a user's travel plans. The second retrieves the conversation so you can check that the message was stored. + +1. Replace your SDK quickstart file with the complete example for your language. For curl, run the commands in order. + +{{< clients-example set="agent_memory_sdk" step="add_session_event" footer="hide" dft_tab_name="curl" runnable="false" try_it="false" >}} +export EVENT_CREATED_AT="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data @- \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/events" <}} + +2. For Python, save the file and run `python quickstart.py`. For TypeScript, save the file and run `npx tsx quickstart.ts`. +3. Find the `events` array in the session response. Check that it contains the travel message, the `USER` role, and the `quickstart-user` actor ID. + +> [!NOTE] +> **What to expect:** The `events` array contains the travel message. Redis Agent Memory adds an `eventId` and `systemTimestamp`, showing that the application can recover the complete event later using only the session ID. + +Run the event write once. Repeating it adds the same message again. The next example searches without repeating the write. + + + +## Recall an extracted preference + +Redis Agent Memory processes session events in the background and creates long-term memories for information that may be useful in later conversations. You configured the extraction cadence to one minute when you created the service. + +Wait at least one minute. Replace your SDK file with the complete search example, or run the curl command: + +{{< clients-example set="agent_memory_sdk" step="search_long_term_memory_basic" footer="hide" dft_tab_name="curl" runnable="false" try_it="false" >}} +curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data @- \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/search" <}} + +Run the search and inspect `items`. Extraction is asynchronous, so run the search again if the array is empty. + +> [!NOTE] +> **What to expect:** Results similar to `User is a vegetarian` and `User prefers spicy food`. Your application did not create these memories directly. Redis Agent Memory derived them from the session event. The exact text and memory types can vary. + +The extracted memory remains searchable after the session expires, subject to the long-term memory time to live (TTL). You can change the extraction cadence and both TTLs in the [Redis Agent Memory service configuration]({{< relref "/operate/iris/agent-memory/create-service#memory-configuration" >}}). + +## Next steps + +Generate a complete conversational agent with the [AI agent builder]({{< relref "/develop/ai/agent-builder" >}}). Select **Redis Iris Conversational Assistant**, then choose **Python** or **JavaScript**. + +Follow the [developer guide]({{< relref "/develop/ai/context-engine/agent-memory/developer-guide" >}}) to use memory in an agent’s request cycle. For specific features, see [Sessions]({{< relref "/develop/ai/context-engine/agent-memory/sessions" >}}), [Custom memory types]({{< relref "/develop/ai/context-engine/agent-memory/long-term-memory#custom-memory-types" >}}), and [Namespaces]({{< relref "/develop/ai/context-engine/agent-memory/namespaces" >}}). diff --git a/content/develop/ai/context-engine/agent-memory/rest-api-quickstart.md b/content/develop/ai/context-engine/agent-memory/rest-api-quickstart.md deleted file mode 100644 index 9a5d00feab..0000000000 --- a/content/develop/ai/context-engine/agent-memory/rest-api-quickstart.md +++ /dev/null @@ -1,255 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- develop -- ai -description: Explore session memory, automatic extraction, summarization, custom memory types, and sensitive-data exclusions with the Redis Agent Memory REST API. -hideListLinks: true -linktitle: REST quickstart -title: Redis Agent Memory REST API quickstart -weight: 8 -aliases: -- /operate/rc/context-engine/agent-memory/use-agent-memory/ -- /develop/ai/context-engine/agent-memory/api-examples/ ---- - -Use this quickstart to follow a travel planning conversation through Redis Agent Memory. You will retrieve the conversation from session memory, recall information extracted in the background, inspect an automatically generated session summary, extract structured travel information, and guide extraction away from sensitive data. - -## Before you begin - -To complete this quickstart, you need: - -* A Redis Cloud account that can create Redis Agent Memory services. -* An eligible Redis Cloud database, or permission to create one. -* A shell with `curl` and `jq` installed. - -## Create a Redis Agent Memory service - -{{< embed-md "rc-agent-memory-quickstart-create-service.md" >}} - -## Save the connection values - -1. Open the Redis Agent Memory service in the Redis Cloud console. -1. On the **Configuration** tab, copy the **Endpoint** and **Store ID**. -1. Export the values in your shell: - - ```sh - export AGENT_MEMORY_URL='' - export STORE_ID='' - export API_KEY='' - export SESSION_ID='travel-planning-session' - export OWNER_ID='quickstart-user' - ``` - -`AGENT_MEMORY_URL` must include `https://`. Keep the API key out of source control, application logs, and other unsecured locations. - -## Check the service health - -Verify that the service is available: - -```sh -curl --fail-with-body --silent --show-error \ - --header "Authorization: Bearer $API_KEY" \ - "$AGENT_MEMORY_URL/health" | jq -``` - -## 1. Build conversation context with session memory - -Session memory stores a conversation as an ordered sequence of events. Add a user message that contains details the travel agent will need later: - -```sh -export EVENT_CREATED_AT="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" - -curl --fail-with-body --silent --show-error \ - --request POST \ - --header "Authorization: Bearer $API_KEY" \ - --header 'Content-Type: application/json' \ - --data @- \ - "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/events" < [!NOTE] -> **What to expect:** The `events` array contains the travel message. Redis Agent Memory adds an `eventId` and `systemTimestamp`, showing that the application can recover the complete event later using only the session ID. - -## 2. Recall automatically extracted information - -Redis Agent Memory processes session events in the background and creates long term memories for information that may be useful in later conversations. You configured the extraction cadence to one minute when you created the service. You do not need to submit a separate memory creation request. - -Wait at least one minute, then search for the user's dietary requirements: - -```sh -curl --fail-with-body --silent --show-error \ - --request POST \ - --header "Authorization: Bearer $API_KEY" \ - --header 'Content-Type: application/json' \ - --data @- \ - "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/search" < [!NOTE] -> **What to expect:** Results similar to `User is a vegetarian` and `User prefers spicy food`. Your application did not create these memories directly. Redis Agent Memory derived them from the session event. The exact text and memory types can vary. - -The extracted memory remains searchable after the session expires, subject to the long term memory TTL. You can change the extraction cadence and both TTLs in the [Redis Agent Memory service configuration](/content/operate/iris/agent-memory/create-service.md#memory-configuration). - -## 3. Keep long conversations concise with automatic summarization - -Automatic summarization condenses older events and retains the most recent events in full. The retrieved session then contains a `summary` object and the recent `events` array, so the application can provide useful history without filling the model's context window with every original message. - -You enabled automatic summarization when you created the service. When the session reaches six events, Redis Agent Memory summarizes the older events and retains the two most recent events in full. - -### Add conversation turns - -Add enough user and assistant events to reach the configured threshold. Use the request from the first step and change `role`, `actorId`, `content`, and `createdAt` for each event. The Python and TypeScript quickstarts use six additional turns about travel dates, restaurant style, and budget. - -Summarization runs in the background after the session reaches the threshold. - -### Retrieve the summarized session - -After a short wait, retrieve the session again: - -```sh -curl --fail-with-body --silent --show-error \ - --header "Authorization: Bearer $API_KEY" \ - "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/$SESSION_ID" | jq -``` - -Repeat the retrieval after a short wait if `summary` is not present. Compare `summary.text` with the recent events. The summary should preserve earlier decisions about the trip while recent turns remain available in full. - -> [!NOTE] -> **What to expect:** A `summary` object that preserves details such as Tokyo, Kyoto, the travel dates, and food preferences. `summarizedUpToEventId` identifies the last event covered by the summary, while `events` contains the newer turns that remain in full. The exact summary text can vary. - -See [automatic summarization configuration](/content/operate/iris/agent-memory/create-service.md#automatic-summarization) for details. - -## 4. Extract business specific data with a custom memory type - -Built in memories preserve generally useful information. Custom memory types let an application extract structured information for its business domain. You configured `trip_preference` when you created the service, so it processed the same travel planning event independently. - -Search for the structured memory: - -```sh -curl --fail-with-body --silent --show-error \ - --request POST \ - --header "Authorization: Bearer $API_KEY" \ - --header 'Content-Type: application/json' \ - --data @- \ - "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/long-term-memory/search" < [!NOTE] -> **What to expect:** A result with `memoryType` set to `trip_preference` that combines the destinations, travel period, and dietary preferences. This shows that the custom type processed the same conversation independently from the built-in memory types. - -See [custom memory types](/content/operate/iris/agent-memory/create-service.md#custom-memory-types) for configuration requirements and limits. - -## 5. Guide extraction away from sensitive data - -The semantic exclusion prompt tells Redis Agent Memory which information should not be kept in long-term memory. Add an event containing a fictional booking code and information that is safe to retain: - -```sh -export EVENT_CREATED_AT="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" - -curl --fail-with-body --silent --show-error \ - --request POST \ - --header "Authorization: Bearer $API_KEY" \ - --header 'Content-Type: application/json' \ - --data @- \ - "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/events" < [!NOTE] -> **What to expect:** A memory similar to `User booked Hotel Sakura in Tokyo` without the fictional confirmation code. If the code appears, refine the exclusion prompt and test again. Exclusions remain advisory. - -> [!WARNING] -> Semantic exclusions are advisory and do not guarantee that sensitive information is excluded. Session content still reaches the extraction model provider. Do not use real sensitive data in this exercise. Exclusions do not apply to directly created long-term memories. - -See [sensitive-data exclusions](/content/operate/iris/agent-memory/create-service.md#sensitive-data-exclusions) for configuration details. - -## Next steps - -* Follow the [Python SDK quickstart](/content/develop/ai/context-engine/agent-memory/python-sdk-quickstart.md) or [TypeScript SDK quickstart](/content/develop/ai/context-engine/agent-memory/typescript-sdk-quickstart.md). -* Learn when to [create long term memories directly](/content/develop/ai/context-engine/agent-memory/developer-guide.md#create-long-term-memories). -* Use the [Redis Agent Memory API reference](/content/develop/ai/context-engine/agent-memory/api-reference.md) for endpoint and schema details. diff --git a/content/develop/ai/context-engine/agent-memory/sessions/_index.md b/content/develop/ai/context-engine/agent-memory/sessions/_index.md new file mode 100644 index 0000000000..b9e7e287b3 --- /dev/null +++ b/content/develop/ai/context-engine/agent-memory/sessions/_index.md @@ -0,0 +1,250 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Add and retrieve conversation events, configure retention, and summarize older messages. +hideListLinks: true +linktitle: Sessions +title: Sessions +weight: 10 +--- + +Session memory holds a conversation's transcript. Your application saves each message as an event, then retrieves the transcript when the user sends the next message. This gives the agent the history it needs to continue the conversation. + +For example, a travel assistant can retrieve the cities a user mentioned earlier in the chat instead of asking again. Each chat has a session ID (`sessionId`) that ties its events together. The same ID lets your application retrieve the conversation later. + +This page covers adding events, retrieving a conversation, and finding session IDs by owner or namespace. It also explains two settings that control the history: retention determines how long a session stays available, and summarization condenses older messages so they take less space in the model's context window. + +## Add session events + +A session event represents one message from a user, assistant, or system. To add one, send `POST /v1/stores/{storeId}/session-memory/events`, or call `add_session_event` in Python or `addSessionEvent` in TypeScript. + +Redis Agent Memory adds the event ID and storage timestamp. The event fields are: + +| Field | Who supplies it | Purpose | +|:------|:----------------|:--------| +| `eventId` | Server-generated. | Identifies the event. | +| `sessionId` | Client-supplied, or server-generated when omitted. | Identifies the session that contains the event. | +| `actorId` | Required from the client. | Identifies the user, agent, or other actor that produced the event. | +| `role` | Required from the client. | Role of the message in the conversation. | +| `content` | Required from the client. | Message content, including its text. | +| `createdAt` | Required from the client. | Time the event occurred in the application. | +| `systemTimestamp` | Server-generated. | Time Redis Agent Memory stored the event. | +| `metadata` | Optional from the client. | Application-specific information associated with the event. | + +1. Set the first event's `actorId` to the signed-in user's account ID from your application. Use the same ID each time this user returns. The server uses it as the session owner. Later events can identify other actors, such as the assistant. +1. Supply a `sessionId`, or omit it to let the server create one. Save the returned `event.sessionId` for later requests. See [Identify users and conversations]({{< relref "/develop/ai/context-engine/agent-memory/developer-guide#identify-users-and-conversations" >}}) for examples. +1. Set the message's `role`, `content`, and `createdAt` timestamp. +1. Send the request. For curl, reuse the connection values from the [quickstart](/content/develop/ai/context-engine/agent-memory/quickstart.md#save-the-connection-values): + +```sh +curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' \ + --data "$(jq -n --arg session "$SESSION_ID" --arg owner "$OWNER_ID" --arg now "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + '{sessionId:$session,actorId:$owner,role:"USER",content:[{text:"I am vegetarian."}],createdAt:$now}')" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/events" | jq +``` + +Send each new message once, using the same session ID throughout the conversation. + +The session's `ownerId` comes from the first event's `actorId` and is returned when you retrieve the session. In on-prem deployments with [server-side ownership enforcement](/content/operate/iris/agent-memory/self-managed/authentication.md#server-side-ownership-enforcement), the first event's `actorId` must match the authenticated user ID. + +## Retrieve conversation context + +When the next message arrives, retrieve the session by its ID before calling the model. For example, reuse the curl connection values from the [quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart#save-the-connection-values" >}}) and run: + +```sh +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $API_KEY" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/$SESSION_ID" | jq +``` + +The `events` array contains the retained conversation messages. If older events have been summarized, the response also includes `summary.text`. Pass both to the model with the current user message. See [Build context for each agent turn]({{< relref "/develop/ai/context-engine/agent-memory/developer-guide#build-context-for-each-agent-turn" >}}) for an example. + +Your application must check that the current user can access this session before retrieving it. The request selects a session by ID; it does not include an owner filter. + +## List sessions + +To let a user reopen an earlier conversation, list their sessions and retrieve the selected session by ID. Use `GET /v1/stores/{storeId}/sessions`, `list_session_summaries` in Python, or `listSessionSummaries` in TypeScript. These rows do not contain events or generated summary text. See [List and sort sessions](/content/develop/ai/context-engine/agent-memory/sessions/list-sessions.md) for SDK examples and pagination. + +For session IDs only, use `GET /v1/stores/{storeId}/session-memory` with an owner or namespace filter. Reuse the curl connection values from the [quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart#save-the-connection-values" >}}). + +For a user's personal conversations, set `filterOwnerId` to that user's authenticated ID. A namespace-only query is suitable for shared conversations only after your application checks access to that namespace. Reserve `includeAll=true` for callers your application authorizes to list the whole store. See [Authorize every memory request](/content/develop/ai/context-engine/agent-memory/developer-guide.md#authorize-every-memory-request). + +To list sessions in one namespace, set `NAMESPACE_ID` to the stable ID returned when you [create a namespace]({{< relref "/develop/ai/context-engine/agent-memory/namespaces#create-a-personal-namespace" >}}): + +```sh +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $API_KEY" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory?namespaceRef=$NAMESPACE_ID" | jq +``` + +`namespaceRef` matches the namespace ID exactly, including case. It excludes descendant namespaces, sessions with only the older `namespace` string label, and sessions without a namespace resource. + +Add `filterOwnerId` to return sessions that match both the namespace and the owner: + +```sh +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $API_KEY" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory?namespaceRef=$NAMESPACE_ID&filterOwnerId=$OWNER_ID" | jq +``` + +You can also use `filterOwnerId` alone. To list all sessions, omit both filters and set `includeAll=true`: + +```sh +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $API_KEY" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory?includeAll=true" | jq +``` + +Do not combine `includeAll=true` with either filter. A request without a filter must set `includeAll=true`. + +The response contains session IDs in `items` and the number of matching sessions in `total`. If the response includes `nextPageToken`, pass it as `pageToken` with the same filters to fetch the next page. See the [List Sessions API reference]({{< relref "/develop/ai/context-engine/agent-memory/api-reference" >}}#operation/ListSessions) for pagination limits. + +## Configure session retention + +The session-memory time to live (TTL) controls how long sessions remain available. Choose a duration that lets users resume conversations for as long as your application needs. When a session expires, its events are no longer available through session-memory retrieval. + +In the Redis Cloud console, open your service's **Configuration** tab and select **Edit**. Set **Short-term TTL** under **Memory configuration**. + +See [memory configuration]({{< relref "/operate/iris/agent-memory/create-service#memory-configuration" >}}) to configure the session-memory TTL in Redis Cloud. + +## Configure automatic summarization + +As a conversation grows, its messages take more space in the model's context window. Automatic summarization replaces older messages with a shorter summary while keeping recent messages in full. + +In the Redis Cloud console, open your service's **Configuration** tab and select **Edit**. Under **Memory configuration**, enable **Automatic summarization** and set: + +* **Summarize after:** The number of messages a session can contain before older messages are summarized. +* **Keep most recent:** The number of recent messages that remain available in full. + +For example, with **Summarize after** set to 20 and **Keep most recent** set to 10, Redis Agent Memory summarizes the older 10 messages when the session reaches 20 messages and retains the 10 most recent messages in full. + +See [automatic summarization]({{< relref "/operate/iris/agent-memory/create-service#automatic-summarization" >}}) to enable summarization and configure both thresholds in Redis Cloud. + +For an example that combines session events with long-term recall, follow the [developer guide]({{< relref "/develop/ai/context-engine/agent-memory/developer-guide" >}}). For complete schemas, see the [session-memory application programming interface (API) reference]({{< relref "/develop/ai/context-engine/agent-memory/api-reference" >}}#operation/GetSessionMemory). + +To group a session and its extracted memories, use an optional [namespace]({{< relref "/develop/ai/context-engine/agent-memory/namespaces#place-and-retrieve-memories" >}}). + +## Try automatic summarization + +{{< embed-md "agent-memory-continue-from-quickstart.md" >}} + +Follow [Configure automatic summarization](#configure-automatic-summarization). Set **Summarize after** to `6` and **Keep most recent** to `2` for this example. The retrieved session will contain a `summary` object and the two most recent events in `events`. + + +### Add conversation turns + +Add these events to the session from the quickstart to continue the conversation past the configured threshold: + +{{< multitabs id="conversation-turns" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + turns = [ + (models.MessageRole.ASSISTANT, "What dates are you traveling?"), + (models.MessageRole.USER, "I arrive on October 10 and leave on October 18."), + (models.MessageRole.ASSISTANT, "Would you like formal or casual restaurants?"), + (models.MessageRole.USER, "Mostly casual places near public transit."), + (models.MessageRole.ASSISTANT, "Do you have a preferred budget?"), + (models.MessageRole.USER, "About 40 euros per person."), + ] + + for role, text in turns: + agent_memory.add_session_event( + session_id=SESSION_ID, + actor_id=USER_ID if role == models.MessageRole.USER else "travel-agent", + role=role, + content=[models.Text(text=text)], + created_at=datetime.now(timezone.utc), + ) +``` + +-tab-sep- + +```typescript + const turns = [ + { role: "ASSISTANT", actorId: "travel-agent", text: "What dates are you traveling?" }, + { role: "USER", actorId: userId, text: "I arrive on October 10 and leave on October 18." }, + { role: "ASSISTANT", actorId: "travel-agent", text: "Would you like formal or casual restaurants?" }, + { role: "USER", actorId: userId, text: "Mostly casual places near public transit." }, + { role: "ASSISTANT", actorId: "travel-agent", text: "Do you have a preferred budget?" }, + { role: "USER", actorId: userId, text: "About 40 euros per person." }, + ] as const; + + for (const turn of turns) { + await agentMemory.addSessionEvent({ + sessionId, + actorId: turn.actorId, + role: turn.role, + content: [{ text: turn.text }], + createdAt: new Date(), + }); + } +``` + +-tab-sep- + +Run this command once to append the six turns: + +```sh +jq -cn --arg user "$OWNER_ID" '[ + {role:"ASSISTANT",actorId:"travel-agent",text:"What dates are you traveling?"}, + {role:"USER",actorId:$user,text:"I arrive on October 10 and leave on October 18."}, + {role:"ASSISTANT",actorId:"travel-agent",text:"Would you like formal or casual restaurants?"}, + {role:"USER",actorId:$user,text:"Mostly casual places near public transit."}, + {role:"ASSISTANT",actorId:"travel-agent",text:"Do you have a preferred budget?"}, + {role:"USER",actorId:$user,text:"About 40 euros per person."} +] | .[]' | while IFS= read -r turn; do + printf '%s' "$turn" | jq --arg session "$SESSION_ID" \ + --arg now "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + '{sessionId:$session,actorId:.actorId,role:.role,content:[{text:.text}],createdAt:$now}' | \ + curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header 'Content-Type: application/json' --data @- \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/events" || exit 1 +done +``` + +{{< /multitabs >}} + +Run these writes once. In the SDK files, comment out the loop before subsequent runs. Summarization runs in the background. + + +### Retrieve the summarized session + +After a short wait, retrieve the session again: + +{{< multitabs id="session-summary" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python + compacted_session = agent_memory.get_session_memory( + session_id=SESSION_ID, + ) + show("Compacted session memory", compacted_session) +``` + +-tab-sep- + +```typescript + const compactedSession = await agentMemory.getSessionMemory(sessionId); + console.log("Compacted session memory:"); + console.dir(compactedSession, { depth: null }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $API_KEY" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/$SESSION_ID" | jq +``` + +{{< /multitabs >}} + +Run the retrieval again after a short wait if `summary` is not present. Compare `summary.text` with the recent events. The summary should preserve earlier trip decisions while recent turns remain available in full. + +> [!NOTE] +> **What to expect:** A `summary` object that preserves details such as Tokyo, Kyoto, the travel dates, and food preferences. `summarizedUpToEventId` identifies the last event covered by the summary, while `events` contains the newer turns that remain in full. The exact summary text can vary. diff --git a/content/develop/ai/context-engine/agent-memory/sessions/list-sessions.md b/content/develop/ai/context-engine/agent-memory/sessions/list-sessions.md new file mode 100644 index 0000000000..d5eeb0363f --- /dev/null +++ b/content/develop/ai/context-engine/agent-memory/sessions/list-sessions.md @@ -0,0 +1,124 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: List session metadata by owner or namespace, sort results, and follow pagination. +hideListLinks: true +linktitle: List sessions +title: List and sort sessions +weight: 10 +aliases: +- /develop/ai/context-engine/agent-memory/list-sessions/ +--- + +Use session listing to let a user reopen an earlier conversation. The session rows contain IDs and available owner, timestamp, and namespace metadata. They do not contain conversation events or summary text. Retrieve the selected session with `get_session_memory` (Python), `getSessionMemory` (TypeScript), or `GET /v1/stores/{storeId}/session-memory/{sessionId}` to build the agent's conversation context. + +## Choose a listing operation + +| Result you need | REST endpoint | Python method | TypeScript method | +|:----------------|:--------------|:--------------|:------------------| +| Session IDs | `GET /v1/stores/{storeId}/session-memory` | `list_sessions` | `listSessions` | +| Session rows with sorting | `GET /v1/stores/{storeId}/sessions` | `list_session_summaries` | `listSessionSummaries` | + +The second method's name refers to metadata rows, not generated conversation summaries. Both operations return `items`, `total`, and an optional `nextPageToken`. + +For a user's personal conversations, set `filterOwnerId` to the user's authenticated ID. The session owner is the first event's `actorId`. Add `namespaceRef` to require an exact namespace ID as well. It does not include descendants, deprecated namespace labels, or sessions without a namespace resource. Owner and namespace matching is case-sensitive. + +A request without filters must set `includeAll=true`. Do not combine `includeAll=true` with either filter. Authorize store-wide or shared namespace listing in your application before making the request. A filter selects records; it does not establish the caller's right to access them. + +## Sort and paginate session rows + +The default is `sortBy=updatedAt` and `sortOrder=desc`, so recent activity appears first. `sortBy` accepts `sessionId`, `createdAt`, or `updatedAt`. When omitted, `sortOrder` is `asc` for `sessionId` and `desc` for timestamps. `limit` accepts 1–1000 and defaults to 100. + +Install `redis-agent-memory` for Python or `@redis-iris/agent-memory` for TypeScript. Set `AGENT_MEMORY_URL`, `STORE_ID`, `API_KEY`, and `OWNER_ID` in your shell. See the [developer guide](/content/develop/ai/context-engine/agent-memory/developer-guide.md#connect-to-a-redis-agent-memory-service) for connection details. + +These examples list all pages for one owner, with the most recently updated sessions first: + +{{< multitabs id="sorted-session-list" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python +import asyncio +import os + +from redis_agent_memory import AgentMemory + + +async def main(): + async with AgentMemory( + os.environ["AGENT_MEMORY_URL"], + store_id=os.environ["STORE_ID"], + api_key=os.environ["API_KEY"], + ) as agent_memory: + page_token = None + while True: + page = await agent_memory.list_session_summaries_async( + filter_owner_id=os.environ["OWNER_ID"], + sort_by="updatedAt", sort_order="desc", + limit=20, page_token=page_token, + ) + for session in page.items: + print(session.model_dump_json(by_alias=True)) + page_token = page.next_page_token + if not page_token: + break + + +asyncio.run(main()) +``` + +-tab-sep- + +```typescript +import { AgentMemory } from "@redis-iris/agent-memory"; + +async function main() { + const { AGENT_MEMORY_URL, STORE_ID, API_KEY, OWNER_ID } = process.env; + if (!AGENT_MEMORY_URL || !STORE_ID || !API_KEY || !OWNER_ID) { + throw new Error("Set AGENT_MEMORY_URL, STORE_ID, API_KEY, and OWNER_ID."); + } + const agentMemory = new AgentMemory({ + serverURL: AGENT_MEMORY_URL, storeId: STORE_ID, apiKey: API_KEY, + }); + let pageToken: string | undefined; + do { + const page = await agentMemory.listSessionSummaries( + 20, // limit + pageToken, // pageToken + OWNER_ID, // filterOwnerId + undefined, // namespaceRef + undefined, // includeAll + "updatedAt", // sortBy + "desc", // sortOrder + ); + console.dir(page.items, { depth: null }); + pageToken = page.nextPageToken; + } while (pageToken); +} + +main().catch((error) => { console.error(error instanceof Error ? error.message : "Request failed"); process.exitCode = 1; }); +``` + +-tab-sep- + +```sh +page_token='' +while :; do + page=$(curl --fail-with-body --silent --show-error --get \ + --header "Authorization: Bearer $API_KEY" \ + --data-urlencode "filterOwnerId=$OWNER_ID" \ + --data-urlencode 'sortBy=updatedAt' --data-urlencode 'sortOrder=desc' \ + --data-urlencode 'limit=20' --data-urlencode "pageToken=$page_token" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/sessions") || break + printf '%s\n' "$page" | jq '.items' + page_token=$(printf '%s\n' "$page" | jq -r '.nextPageToken // empty') + [ -n "$page_token" ] || break +done +``` + +{{< /multitabs >}} + +Pass each returned `nextPageToken` as `pageToken`. Keep filters and sort options unchanged; the token is bound to them. Stop when the token is absent. The token is opaque, so URL-encode it in REST requests. Changing a filter or sort requires a new request without the old token. + +Sessions stored without owner or timestamp metadata can omit `ownerId`, `createdAt`, or `updatedAt`. Handle those missing values when rendering a conversation list. A row's timestamps describe session metadata; they do not promise that background extraction or summarization has completed. diff --git a/content/develop/ai/context-engine/agent-memory/summary-views.md b/content/develop/ai/context-engine/agent-memory/summary-views.md new file mode 100644 index 0000000000..5028b1bd79 --- /dev/null +++ b/content/develop/ai/context-engine/agent-memory/summary-views.md @@ -0,0 +1,274 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Create owned summaries of long-term memories, refresh them, and read text or structured results. +hideListLinks: true +linktitle: Summary Views +title: Summarize long-term memories with Summary Views +weight: 14 +--- + +A Summary View turns selected long-term memories into a reusable text or structured summary. For example, a travel assistant can maintain a compact profile instead of adding every matching memory to each model request. + +A **definition** specifies the sources, filters, grouping, output, and refresh policy. A **partition** is one materialized result for an exact combination of grouping values. An ungrouped definition has one result. Results are derived data; they do not enter long-term-memory search or replace the source records. + +Summary Views differ from [session summarization](/content/develop/ai/context-engine/agent-memory/sessions/_index.md#configure-automatic-summarization), which compacts conversation events. The supported source is `long_term`. Document-managed MemoryETL records are excluded from Summary View sources. + +## Prepare the service and identity + +Summary Views are available in Redis Cloud and enabled in self-managed deployments. For self-managed model and capacity settings, see [Configure Summary Views](/content/operate/iris/agent-memory/self-managed/configuration.md#enable-summary-views). + +The examples use a self-managed service with ownership enforcement and [delegated user identity](/content/operate/iris/agent-memory/self-managed/authentication.md#delegate-a-user-with-an-agent-key). Set `AGENT_MEMORY_URL`, `STORE_ID`, `API_KEY`, `USER_TOKEN`, and `OWNER_ID` as described there. `OWNER_ID` must match the verified user claim. Install the SDK through the [quickstart](/content/develop/ai/context-engine/agent-memory/quickstart.md#install-the-sdk). + +The server assigns the definition's immutable `ownerId`. In on-prem deployments, creation requires an authenticated user ID even with enforcement off. A store credential without user identity cannot create a view by supplying an owner field. + +### Choose private or shared visibility + +In on-prem deployments with ownership enforcement enabled: + +| Visibility | Who can read | Sources | +|:-----------|:-------------|:--------| +| `PRIVATE` (default) | The definition owner. | The owner's personal and store-authorized shared memories, narrowed by the definition's filters. | +| `SHARED` | Callers with store read access. The owner retains lifecycle control. | Shared memories only. Personal records cannot be published through a shared view. | + +Shared does not mean a particular team. Enforce team membership in your backend or use separate stores and grants. In on-prem deployments with enforcement off, store-level access is broader: `PRIVATE` does not replace backend authorization. A view records its source-access policy at creation. Changing on-prem enforcement requires replacing existing views and configuring server and worker alike. + +## Create, refresh, and read a view + +These examples create one private profile for `OWNER_ID`, request a full refresh, and poll its result. First [create ordinary long-term memories](/content/develop/ai/context-engine/agent-memory/long-term-memory.md#create-a-custom-memory-directly) in this self-managed store, using the delegated client and matching `OWNER_ID`. A successful refresh with no matching sources returns `empty`. + +Save the view ID for later requests. Creation and refresh are asynchronous; their responses do not contain generated output. + +{{< multitabs id="summary-view-lifecycle" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python +import asyncio +import os + +from redis_agent_memory import AgentMemory + + +async def main(): + headers = {"X-Iris-User-Token": os.environ["USER_TOKEN"]} + async with AgentMemory( + os.environ["AGENT_MEMORY_URL"], store_id=os.environ["STORE_ID"], + api_key=os.environ["API_KEY"], + ) as memory: + view = await memory.create_summary_view_async( + name="Travel profile", source="long_term", group_by=[], + filter_={"owner_id": {"eq": os.environ["OWNER_ID"]}}, + output={"format_": "text", "instructions": "Summarize the user's travel preferences."}, + refresh_policy={"mode": "manual"}, http_headers=headers, + ) + print("View ID:", view.id) + accepted = await memory.refresh_summary_view_async( + view_id=view.id, target="full", http_headers=headers, + ) + print(accepted.model_dump_json(by_alias=True)) + for _ in range(60): + result = await memory.get_summary_view_result_async( + view_id=view.id, include_source_memory_ids=True, http_headers=headers, + ) + if result.status == "failed": + raise RuntimeError(f"Refresh failed: {result.error_code}") + if result.status == "empty": + print("No matching source memories.") + return + if result.status == "ready" and not result.stale: + if result.value is None: + raise RuntimeError("Ready result omitted its value") + print(result.model_dump_json(by_alias=True)) + return + await asyncio.sleep(2) + raise TimeoutError(f"Continue polling view {view.id}") + + +asyncio.run(main()) +``` + +-tab-sep- + +```typescript +import { setTimeout as delay } from "node:timers/promises"; +import { AgentMemory } from "@redis-iris/agent-memory"; + +async function main() { + const { AGENT_MEMORY_URL, STORE_ID, API_KEY, USER_TOKEN, OWNER_ID } = process.env; + if (!AGENT_MEMORY_URL || !STORE_ID || !API_KEY || !USER_TOKEN || !OWNER_ID) { + throw new Error("Set the connection values, USER_TOKEN, and OWNER_ID."); + } + const memory = new AgentMemory({ serverURL: AGENT_MEMORY_URL, storeId: STORE_ID, apiKey: API_KEY }); + const options = { headers: { "X-Iris-User-Token": USER_TOKEN } }; + const view = await memory.createSummaryView({ + name: "Travel profile", source: "long_term", groupBy: [], + filter: { ownerId: { eq: OWNER_ID } }, + output: { format: "text", instructions: "Summarize the user's travel preferences." }, + refreshPolicy: { mode: "manual" }, + }, options); + console.log("View ID:", view.id); + console.dir(await memory.refreshSummaryView(view.id, { target: "full" }, options)); + for (let attempt = 0; attempt < 60; attempt++) { + const result = await memory.getSummaryViewResult( + view.id, // viewId + undefined, // ownerId + undefined, // sessionId + undefined, // namespaceRef + undefined, // memoryType + true, // includeSourceMemoryIds + options, // request options + ); + if (result.status === "failed") throw new Error(`Refresh failed: ${result.errorCode}`); + if (result.status === "empty") { + console.log("No matching source memories."); + return; + } + if (result.status === "ready" && !result.stale) { + if (!result.value) throw new Error("Ready result omitted its value"); + console.dir(result, { depth: null }); + return; + } + await delay(2000); + } + throw new Error(`Continue polling view ${view.id}`); +} + +main().catch((error) => { console.error(error instanceof Error ? error.message : "Request failed"); process.exitCode = 1; }); +``` + +-tab-sep- + +```sh +definition=$(jq -n --arg owner "$OWNER_ID" \ + '{name:"Travel profile",source:"long_term",groupBy:[],filter:{ownerId:{eq:$owner}},output:{format:"text",instructions:"Summarize travel preferences."},refreshPolicy:{mode:"manual"}}') +view=$(curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: $USER_TOKEN" \ + --header 'Content-Type: application/json' --data "$definition" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/summary-views") || exit 1 +VIEW_ID=$(printf '%s' "$view" | jq -er .id) || exit 1 +printf 'View ID: %s\n' "$VIEW_ID" + +curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: $USER_TOKEN" \ + --header 'Content-Type: application/json' --data '{"target":"full"}' \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/summary-views/$VIEW_ID/refresh" | jq + +for attempt in $(seq 1 60); do + result=$(curl --fail-with-body --silent --show-error --get \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: $USER_TOKEN" \ + --data-urlencode 'includeSourceMemoryIds=true' \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/summary-views/$VIEW_ID/result") || exit 1 + state=$(printf '%s' "$result" | jq -r .status) + case "$state" in + failed) printf '%s\n' "$result" | jq; exit 1 ;; + empty) printf 'No matching source memories.\n'; exit 0 ;; + ready) + if printf '%s' "$result" | jq -e '.stale == false and .value != null' >/dev/null; then + printf '%s\n' "$result" | jq + exit 0 + fi ;; + esac + sleep 2 +done +printf 'Continue polling view %s\n' "$VIEW_ID" +exit 1 +``` + +{{< /multitabs >}} + +Text output uses `value.text`; structured output uses `value.data`. Python output configuration uses `format_` and `schema_`, serialized as `format` and `schema`. Do not assume every result has a `value`. + +### Handle asynchronous result states + +| Status | Application behavior | +|:-------|:---------------------| +| `cold` | No result is stored for this group. It may not have been generated yet, or it may have been evicted. | +| `pending` | Work is queued. Previous output may be withheld because its sources changed or became inaccessible. | +| `ready` | A value is available. Check `stale` and coverage fields. | +| `empty` | No sources match. There is no generated value. | +| `refreshing` | New work is running. A last good value can remain. | +| `failed` | Inspect `errorCode`. A last good value may remain, or `value` may be absent. | + +`stale` is independent of status. It compares the result with the current definition and source changes. Decide whether to use a last good stale value, wait, or fall back to direct recall. Do not turn an absent value into an empty summary. + +Result reads validate contributing source records. Deleted, changed, or newly inaccessible sources can cause old output to be withheld and repair to be requested. Repair is asynchronous; an old value need not remain available. + +## Filter and group sources + +Use `filter` for owners, sessions, stable namespace IDs, topics, memory types, creation times, and typed attributes. `filterOp` defaults to `all`; `any` combines optional filters with OR. Exact partition scope and time-window restrictions apply separately, as do ownership checks in on-prem deployments. Legacy `namespace` labels are unsupported for Summary Views; use `namespaceRef`. + +`groupBy` accepts up to four distinct fields: `ownerId`, `sessionId`, `namespaceRef`, and `memoryType`. A result read or `target: partition` refresh must supply exactly those fields. Missing or extra scope fields are invalid. `groupBy: []` creates a singleton; omit all scope fields when reading it. + +For `groupBy: ["ownerId", "namespaceRef"]`, use this partition refresh body: + +```json +{ + "target": "partition", + "scope": {"ownerId":"","namespaceRef":""} +} +``` + +Send the same scope as query parameters on result reads. Here `namespaceRef` is an ID string, not the `{"namespaceId":"..."}` object used to place a memory. `target: full` reconciles current and previous partitions. + +`evictionPolicy.maxPartitions` bounds grouped results within the deployment limit. Grouped views use the operator's partition limit when the policy is omitted; singleton views omit it. Matching source mutations make a scope recent; reads do not. Evicted scopes can return `cold`. `timeWindowSeconds` selects sources by creation time in a rolling window; it does not change source retention. + +## Choose structured output and continuous refresh + +For structured output, replace `output` with: + +```json +{ + "format": "structured", + "instructions": "Summarize only preferences supported by the selected memories.", + "schema": { + "fields": [ + {"name":"cities","type":"list[str]","description":"Cities the user wants to visit"}, + {"name":"vegetarian","type":"bool","description":"Whether the user has stated a vegetarian diet"} + ] + } +} +``` + +The schema accepts 1–50 fields of type `str`, `int`, `float`, `bool`, `list[str]`, `list[float]`, or `object`. Generated fields are optional. Arbitrary JSON Schema, required-field declarations, and nested object schemas are unsupported. Text output must omit `schema`; structured output requires it. Invalid generated data is reported as `OUTPUT_VALIDATION_FAILED`. + +For continuous refresh, use: + +```json +{"mode":"continuous","debounceSeconds":5,"reconcileIntervalSeconds":60} +``` + +The reconcile interval is required and must fit deployment limits. Debounce coalesces mutations; reconciliation also handles changes such as a moving time window. Manual mode omits both interval fields. Both modes run asynchronously within queue, source, generation, and concurrency limits. + +## Check coverage and manage definitions + +Inspect `sourceMemoryCount` and `summarizedMemoryCount`. Selection can omit whole records (`sourcesOmitted`) or clip included content (`sourceContentTruncated`). `truncated` combines both. Check `truncated` even when every record was included: some text may have been cut. Concurrent writes can change the source set while counts are measured. + +Request `includeSourceMemoryIds=true` for the selected source IDs. These identify contributing records, not every match. `computedAt`, `sourceUpdatedAt`, and `definitionVersion` help assess freshness. + +| Action | Python method | TypeScript method | REST | +|:-------|:--------------|:------------------|:-----| +| List definitions | `list_summary_views_async` | `listSummaryViews` | `GET /summary-views` | +| Read a definition | `get_summary_view_async` | `getSummaryView` | `GET /summary-views/{viewId}` | +| Update a definition | `update_summary_view_async` | `updateSummaryView` | `PATCH /summary-views/{viewId}` | +| Delete a definition | `delete_summary_view_async` | `deleteSummaryView` | `DELETE /summary-views/{viewId}` | + +REST paths are relative to `/v1/stores/{storeId}`. Lists return `items` and optional `nextPageToken`; pass it as `pageToken` with the same query. Definition states (`active`, `disabled`, `deleting`) are separate from result status. + +| Change | Effect | +|:-------|:-------| +| Name or description | Does not trigger generation. | +| Filter, output, or refresh policy | Increments `definitionVersion`. | +| `groupBy` | Increments `definitionVersion` and replaces the partition layout. | +| Owner, visibility, or source | Cannot be changed. Create a new definition. | +| `state: disabled` | Disables the view. Re-enabling requests refresh work. | + +`clearFilter: true` and `clearTimeWindow: true` remove those settings; do not also supply replacements. + +Deletion returns `202` while cleanup runs. Repeated deletion is idempotent; the definition becomes `deleting` before schedules and results are removed. Source memories remain. + +For quota failures, inspect the API error and deployment limits. For generation or source-access failures, keep the view ID and address the reported error instead of creating duplicates. [Ingestion webhooks](/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md) can reject generated content before publication. diff --git a/content/develop/ai/context-engine/agent-memory/typescript-sdk-quickstart.md b/content/develop/ai/context-engine/agent-memory/typescript-sdk-quickstart.md deleted file mode 100644 index 3edb401d7f..0000000000 --- a/content/develop/ai/context-engine/agent-memory/typescript-sdk-quickstart.md +++ /dev/null @@ -1,321 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- develop -- ai -description: Explore session memory, automatic extraction, summarization, custom memory types, and sensitive-data exclusions with the Redis Agent Memory TypeScript SDK. -hideListLinks: true -linktitle: TypeScript SDK quickstart -title: Redis Agent Memory TypeScript SDK quickstart -weight: 7 ---- - -Use this quickstart to follow a travel planning conversation through Redis Agent Memory. You will retrieve the conversation from session memory, recall information extracted in the background, inspect an automatically generated session summary, extract structured travel information, and guide extraction away from sensitive data. - -## Before you begin - -To complete this quickstart, you need: - -{{< embed-md "rc-agent-memory-quickstart-prerequisites.md" >}} - -You also need Node.js and npm. - -## Create a Redis Agent Memory service - -1. Sign in to the [Redis Cloud console](https://cloud.redis.io/). -1. Select **Agent Memory** from the navigation menu. -1. If Redis Cloud displays the public preview terms, review and accept them. -1. Select **Create custom service**. -1. Enter a service name, select an eligible database, and select its `default` user. -1. Under **Memory configuration**, enter these values: - - | Setting | Value | What it controls | - |:--------|:------|:-----------------| - | **Short-term TTL** | `1` day | How long session memory is retained. | - | **Long-term TTL** | `365` days | How long long-term memories are retained. | - | **Extraction cadence** | `1` minute | How often session events are processed for extraction. One minute is for this quickstart; use a longer production interval unless you need rapid extraction. | - | **Automatic summarization** | Enabled | Whether older session events are condensed into a summary. | - | **Summarize after (messages)** | `6` | The event count that triggers summarization. Six is for this quickstart; use a higher production threshold. | - | **Keep most recent (messages)** | `2` | How many recent events remain in full. Two is for this quickstart; retain more in production when recent turns are needed. | - -1. Under **Memory types & extraction**, select **Add type** and configure this custom memory type: - - | Setting | Value | What it controls | - |:--------|:------|:-----------------| - | **Name** | `trip_preference` | The identifier stored in `memoryType` and used in search filters. | - | **Description** | `Structured requirements for a planned trip` | The purpose of the custom memory type. | - | **Extraction prompt** | `Extract trip requirements only when the user states a destination or travel plan. Preserve explicit dietary requirements and food preferences.` | When to create the memory and which information to capture. | - | **Enabled** | Enabled | Whether new memories of this type are extracted. | - -1. Add these custom fields: - - | Field | Type | Description | - |:------|:-----|:------------| - | `destinations` | `list[str]` | Cities or countries the user plans to visit. | - | `travel_period` | `str` | When the user plans to travel. | - | `dietary_requirements` | `list[str]` | Dietary requirements that affect recommendations. | - | `food_preferences` | `list[str]` | Cuisines, flavors, or dining preferences stated by the user. | - -1. Under **Sensitive-data exclusions**, enable **Semantic exclusions** and enter this exclusion prompt: - - ```text - Do not keep passwords, access tokens, recovery codes, payment card information, or booking confirmation codes in long-term memory. - ``` - -1. Select **Create**. -1. Copy the Redis Agent Memory API key and store it securely. - -> [!WARNING] -> Redis Cloud displays the Redis Agent Memory API key only once. If you lose it, [generate a new API key](/content/operate/iris/agent-memory/view-service.md#replace-service-api-key). - -These settings keep the background stages short enough to observe during the quickstart. For screenshots and configuration details, see [create a Redis Agent Memory service](/content/operate/iris/agent-memory/create-service.md). - -> [!WARNING] -> Sensitive-data exclusions guide the extraction model but do not guarantee exclusion. Sensitive session content still reaches the model provider. Exclusions do not apply when an application creates long-term memories directly. - -## Save the connection values - -1. Open the Redis Agent Memory service in the Redis Cloud console. -1. On the **Configuration** tab, copy the **Endpoint** and **Store ID**. -1. Export the API key in your shell: - - ```sh - export API_KEY='' - ``` - -Keep the API key out of source control, application logs, and other unsecured locations. - -## Install the SDK - -```sh -mkdir agent-memory-quickstart -cd agent-memory-quickstart -npm init -y -npm install @redis-iris/agent-memory -npm install --save-dev tsx -``` - -## Create the client and check the service health - -Create `quickstart.ts` with the following code. Replace `` and `` with the values from Redis Cloud. The endpoint must include `https://`. - -```typescript -import { AgentMemory } from "@redis-iris/agent-memory"; - -const apiKey = process.env.API_KEY; -if (!apiKey) { - throw new Error("Set the API_KEY environment variable."); -} - -const serverURL = ""; -const storeId = ""; -const sessionId = "travel-planning-session"; -const userId = "quickstart-user"; - -const agentMemory = new AgentMemory({ - serverURL, - storeId, - apiKey, -}); - -async function run() { - const health = await agentMemory.health(); - console.log("Service health:"); - console.dir(health, { depth: null }); -} - -run().catch((error) => { - console.error(error); - process.exitCode = 1; -}); -``` - -Run the file: - -```sh -npx tsx quickstart.ts -``` - -A healthy response confirms that the client can reach Redis Agent Memory and authenticate with the API key. The first store request validates the Store ID. - -## 1. Build conversation context with session memory - -Session memory stores a conversation as an ordered sequence of events. Add the following code after the health check, inside `run`: - -```typescript - const event = await agentMemory.addSessionEvent({ - sessionId, - actorId: userId, - role: "USER", - content: [{ - text: "I am visiting Tokyo and Kyoto next month. I am vegetarian and prefer spicy food.", - }], - createdAt: new Date(), - }); - console.log("Created event:"); - console.dir(event, { depth: null }); - - const session = await agentMemory.getSessionMemory(sessionId); - console.log("Session memory:"); - console.dir(session, { depth: null }); -``` - -Run the file again. The session response contains the stored message, its role, actor, and timestamps. An application can retrieve this session before the next agent turn and add the events to the model's context. - -> [!NOTE] -> **What to expect:** The `events` array contains the travel message. Redis Agent Memory adds an `eventId` and `systemTimestamp`, showing that the application can recover the complete event later using only the session ID. - -After the event is stored, comment out the call to `addSessionEvent` before subsequent runs to avoid adding the same message again. - -## 2. Recall automatically extracted information - -Redis Agent Memory processes session events in the background and creates long term memories for information that may be useful in later conversations. You configured the extraction cadence to one minute when you created the service. You do not need to call a memory creation method. - -Wait at least one minute, then add this search after the session retrieval: - -```typescript - const results = await agentMemory.searchLongTermMemory({ - text: "What dietary requirements and food preferences does the user have?", - filter: { - ownerId: { - eq: userId, - }, - }, - limit: 5, - }); - console.log("Automatically extracted memories:"); - console.dir(results, { depth: null }); -``` - -Run the file. The `items` array should contain memories derived from the conversation, such as the vegetarian requirement or preference for spicy food. Extraction is asynchronous, so run the search again if the array is empty. - -> [!NOTE] -> **What to expect:** Results similar to `User is a vegetarian` and `User prefers spicy food`. Your application did not create these memories directly. Redis Agent Memory derived them from the session event. The exact text and memory types can vary. - -The extracted memory remains searchable after the session expires, subject to the long term memory TTL. You can change the extraction cadence and both TTLs in the [Redis Agent Memory service configuration](/content/operate/iris/agent-memory/create-service.md#memory-configuration). - -## 3. Keep long conversations concise with automatic summarization - -Automatic summarization condenses older events and retains the most recent events in full. The retrieved session then contains a `summary` object and the recent `events` array, so the application can provide useful history without filling the model's context window with every original message. - -You enabled automatic summarization when you created the service. When the session reaches six events, Redis Agent Memory summarizes the older events and retains the two most recent events in full. - -### Add conversation turns - -Add this code after the first session event to continue the conversation past the configured threshold: - -```typescript - const turns = [ - { role: "ASSISTANT", actorId: "travel-agent", text: "What dates are you traveling?" }, - { role: "USER", actorId: userId, text: "I arrive on October 10 and leave on October 18." }, - { role: "ASSISTANT", actorId: "travel-agent", text: "Would you like formal or casual restaurants?" }, - { role: "USER", actorId: userId, text: "Mostly casual places near public transit." }, - { role: "ASSISTANT", actorId: "travel-agent", text: "Do you have a preferred budget?" }, - { role: "USER", actorId: userId, text: "About 40 euros per person." }, - ] as const; - - for (const turn of turns) { - await agentMemory.addSessionEvent({ - sessionId, - actorId: turn.actorId, - role: turn.role, - content: [{ text: turn.text }], - createdAt: new Date(), - }); - } -``` - -Run the code once, then comment out the loop to avoid adding the same turns again. Summarization runs in the background. - -### Retrieve the summarized session - -After a short wait, retrieve the session again: - -```typescript - const compactedSession = await agentMemory.getSessionMemory(sessionId); - console.log("Compacted session memory:"); - console.dir(compactedSession, { depth: null }); -``` - -Run the retrieval again after a short wait if `summary` is not present. Compare `summary.text` with the recent events. The summary should preserve earlier trip decisions while recent turns remain available in full. - -> [!NOTE] -> **What to expect:** A `summary` object that preserves details such as Tokyo, Kyoto, the travel dates, and food preferences. `summarizedUpToEventId` identifies the last event covered by the summary, while `events` contains the newer turns that remain in full. The exact summary text can vary. - -See [automatic summarization configuration](/content/operate/iris/agent-memory/create-service.md#automatic-summarization) for details. - -## 4. Extract business specific data with a custom memory type - -Built in memories preserve generally useful information. Custom memory types let an application extract structured information for its business domain. You configured `trip_preference` when you created the service, so it processed the same travel planning event independently. - -Search for the structured memory: - -```typescript - const customResults = await agentMemory.searchLongTermMemory({ - text: "What are the requirements for the user's trip?", - filter: { - ownerId: { eq: userId }, - memoryType: { eq: "trip_preference" }, - }, - limit: 5, - }); - console.log("Trip preference memories:"); - console.dir(customResults, { depth: null }); -``` - -The result uses `trip_preference` as its `memoryType` and contains travel information extracted from the conversation. The exact text and returned fields depend on the conversation, extraction model, and client. - -> [!NOTE] -> **What to expect:** A result with `memoryType` set to `trip_preference` that combines the destinations, travel period, and dietary preferences. This shows that the custom type processed the same conversation independently from the built-in memory types. - -See [custom memory types](/content/operate/iris/agent-memory/create-service.md#custom-memory-types) for configuration requirements and limits. - -## 5. Guide extraction away from sensitive data - -The semantic exclusion prompt tells Redis Agent Memory which information should not be kept in long-term memory. Add an event containing a fictional booking code and information that is safe to retain: - -```typescript - const sensitiveEvent = await agentMemory.addSessionEvent({ - sessionId, - actorId: userId, - role: "USER", - content: [{ - text: "I booked Hotel Sakura in Tokyo. For this example, the fictional booking confirmation code is DEMO-7QX9.", - }], - createdAt: new Date(), - }); - console.log("Event with excluded information:"); - console.dir(sensitiveEvent, { depth: null }); -``` - -Run the code once, then comment out the call to `addSessionEvent`. Wait at least one minute and search for the safe hotel information: - -```typescript - const exclusionResults = await agentMemory.searchLongTermMemory({ - text: "Where is the user staying in Tokyo?", - filter: { - ownerId: { eq: userId }, - }, - limit: 5, - }); - console.log("Memories after semantic exclusion:"); - console.dir(exclusionResults, { depth: null }); -``` - -Inspect the returned memories. They can retain the hotel name, but should not contain `DEMO-7QX9` because the exclusion prompt covers booking confirmation codes. - -> [!NOTE] -> **What to expect:** A memory similar to `User booked Hotel Sakura in Tokyo` without the fictional confirmation code. If the code appears, refine the exclusion prompt and test again. Exclusions remain advisory. - -> [!WARNING] -> Semantic exclusions are advisory and do not guarantee that sensitive information is excluded. Session content still reaches the extraction model provider. Do not use real sensitive data in this exercise. Exclusions do not apply to directly created long-term memories. - -See [sensitive-data exclusions](/content/operate/iris/agent-memory/create-service.md#sensitive-data-exclusions) for configuration details. - -## Next steps - -* Review the [TypeScript SDK package and reference](https://www.npmjs.com/package/@redis-iris/agent-memory). -* Generate a complete conversational agent with the [AI agent builder](/content/develop/ai/agent-builder/_index.md). Select **Redis Iris Conversational Assistant** and **JavaScript**. -* Try the [Python SDK quickstart](/content/develop/ai/context-engine/agent-memory/python-sdk-quickstart.md) or [REST API quickstart](/content/develop/ai/context-engine/agent-memory/rest-api-quickstart.md). -* Learn when to [create long term memories directly](/content/develop/ai/context-engine/agent-memory/developer-guide.md#create-long-term-memories). diff --git a/content/embeds/agent-memory-continue-from-quickstart.md b/content/embeds/agent-memory-continue-from-quickstart.md new file mode 100644 index 0000000000..6ea7fc3939 --- /dev/null +++ b/content/embeds/agent-memory-continue-from-quickstart.md @@ -0,0 +1,7 @@ +These examples continue from the [quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}). Reuse its client, connection values, and user ID. + +- **Python:** Add each snippet inside the `with` block in `main`. +- **TypeScript:** Add each snippet inside `run`. +- **curl:** Use the shell where you exported the connection values. + +Each run executes every write still in the file. Comment out completed writes and their output statements before rerunning it. Repeating a session-event write can add the same message again. diff --git a/content/embeds/rc-agent-memory-get-started.md b/content/embeds/rc-agent-memory-get-started.md index a91027b30a..d21b18c4fa 100644 --- a/content/embeds/rc-agent-memory-get-started.md +++ b/content/embeds/rc-agent-memory-get-started.md @@ -1,3 +1,3 @@ -Follow the [Redis Cloud setup guide]({{< relref "/operate/iris/agent-memory/create-service" >}}) to create a Redis Agent Memory service. Then use the [REST API quickstart]({{< relref "/develop/ai/context-engine/agent-memory/rest-api-quickstart" >}}) to make your first session-memory and long-term-memory requests. +Follow the [Redis Cloud setup guide]({{< relref "/operate/iris/agent-memory/create-service" >}}) to create a Redis Agent Memory service. Then use the [quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}) to make your first session-memory and long-term-memory requests. After you set up Redis Agent Memory, you can [view and manage your service]({{< relref "/operate/iris/agent-memory/view-service" >}}). diff --git a/content/embeds/rc-agent-memory-quickstart-create-service.md b/content/embeds/rc-agent-memory-quickstart-create-service.md index 83d37167a0..44a2eeb6a0 100644 --- a/content/embeds/rc-agent-memory-quickstart-create-service.md +++ b/content/embeds/rc-agent-memory-quickstart-create-service.md @@ -1,6 +1,6 @@ 1. Sign in to the [Redis Cloud console](https://cloud.redis.io/). 1. Select **Agent Memory** from the navigation menu. -1. If Redis Cloud displays the public preview terms, review and accept them. +1. If Redis Cloud displays service terms, review and accept them. 1. Select **Create custom service**. 1. Enter a service name, select an eligible database, and select its `default` user. 1. Under **Memory configuration**, enter these values: @@ -10,33 +10,6 @@ | **Short-term TTL** | `1` day | How long session memory is retained. | | **Long-term TTL** | `365` days | How long long-term memories are retained. | | **Extraction cadence** | `1` minute | How often session events are processed for extraction. One minute is for this quickstart; use a longer production interval unless you need rapid extraction. | - | **Automatic summarization** | Enabled | Whether older session events are condensed into a summary. | - | **Summarize after (messages)** | `6` | The event count that triggers summarization. Six is for this quickstart; use a higher production threshold. | - | **Keep most recent (messages)** | `2` | How many recent events remain in full. Two is for this quickstart; retain more in production when recent turns are needed. | - -1. Under **Memory types & extraction**, select **Add type** and configure this custom memory type: - - | Setting | Value | What it controls | - |:--------|:------|:-----------------| - | **Name** | `trip_preference` | The identifier stored in `memoryType` and used in search filters. | - | **Description** | `Structured requirements for a planned trip` | The purpose of the custom memory type. | - | **Extraction prompt** | `Extract trip requirements only when the user states a destination or travel plan. Preserve explicit dietary requirements and food preferences.` | When to create the memory and which information to capture. | - | **Enabled** | Enabled | Whether new memories of this type are extracted. | - -1. Add these custom fields: - - | Field | Type | Description | - |:------|:-----|:------------| - | `destinations` | `list[str]` | Cities or countries the user plans to visit. | - | `travel_period` | `str` | When the user plans to travel. | - | `dietary_requirements` | `list[str]` | Dietary requirements that affect recommendations. | - | `food_preferences` | `list[str]` | Cuisines, flavors, or dining preferences stated by the user. | - -1. Under **Sensitive-data exclusions**, enable **Semantic exclusions** and enter this exclusion prompt: - - ```text - Do not keep passwords, access tokens, recovery codes, payment card information, or booking confirmation codes in long-term memory. - ``` 1. Select **Create**. 1. Copy the Redis Agent Memory API key and store it securely. @@ -44,7 +17,4 @@ > [!WARNING] > Redis Cloud displays the Redis Agent Memory API key only once. If you lose it, [generate a new API key]({{< relref "/operate/iris/agent-memory/view-service#replace-service-api-key" >}}). -These settings keep the background stages short enough to observe during the quickstart. For screenshots and configuration details, see [create a Redis Agent Memory service]({{< relref "/operate/iris/agent-memory/create-service" >}}). - -> [!WARNING] -> Sensitive-data exclusions guide the extraction model but do not guarantee exclusion. Sensitive session content still reaches the model provider. Exclusions do not apply when an application creates long-term memories directly. +The one-minute extraction interval makes the result easier to observe in this example. For screenshots and configuration details, see [create a Redis Agent Memory service]({{< relref "/operate/iris/agent-memory/create-service" >}}). diff --git a/content/operate/iris/agent-memory/_index.md b/content/operate/iris/agent-memory/_index.md index 83cc23a099..affa13b64d 100644 --- a/content/operate/iris/agent-memory/_index.md +++ b/content/operate/iris/agent-memory/_index.md @@ -9,7 +9,6 @@ categories: - iris weight: 10 hideListLinks: true -bannerText: Redis Agent Memory on Redis Cloud is currently available as a public preview. Features and behavior are subject to change. --- Redis Agent Memory provides persistent session memory and long-term memory for AI agents and applications. diff --git a/content/operate/iris/agent-memory/create-service.md b/content/operate/iris/agent-memory/create-service.md index 53cea6e0a3..ddad9d8b35 100644 --- a/content/operate/iris/agent-memory/create-service.md +++ b/content/operate/iris/agent-memory/create-service.md @@ -9,7 +9,6 @@ hideListLinks: true linktitle: Create service title: Create an Agent Memory service weight: 5 -bannerText: Redis Agent Memory on Redis Cloud is currently available as a public preview. Features and behavior are subject to change. aliases: - /operate/rc/context-engine/agent-memory/create-service/ - /operate/rc/agent-memory/create-service/ @@ -22,7 +21,7 @@ Redis Agent Memory provides a persistent, structured memory layer that AI agents To create a Redis Agent Memory service, you will need a Redis Cloud database. If you don't have one, see [Create a database]({{< relref "/operate/rc/databases/create-database" >}}). -During public preview, Redis Agent Memory doesn't support [Redis Flex]({{< relref "operate/rc/databases/create-database/create-flex-database">}}) databases, databases using [AWS PrivateLink]({{< relref "operate/rc/security/aws-privatelink">}}) connectivity, [Active-Active]({{< relref "/operate/rc/databases/active-active" >}}) databases, or databases with the [default user]({{< relref "/operate/rc/security/access-control/data-access-control/default-user" >}}) turned off. +Redis Agent Memory doesn't support [Redis Flex]({{< relref "operate/rc/databases/create-database/create-flex-database">}}) databases, databases using [AWS PrivateLink]({{< relref "operate/rc/security/aws-privatelink">}}) connectivity, [Active-Active]({{< relref "/operate/rc/databases/active-active" >}}) databases, or databases with the [default user]({{< relref "/operate/rc/security/access-control/data-access-control/default-user" >}}) turned off. ## Create an Agent Memory service @@ -51,7 +50,7 @@ From here: > > If you lose the service key value, you will need to [generate a new service key]({{< relref "/operate/iris/agent-memory/view-service#replace-service-api-key" >}}) to be able to use the Agent Memory API. - After Redis Cloud creates your service, [continue with the REST API quickstart]({{< relref "/develop/ai/context-engine/agent-memory/rest-api-quickstart" >}}). + After Redis Cloud creates your service, [continue with the quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}). - If you want to customize your Redis Agent Memory service, select **Create custom**. @@ -76,7 +75,7 @@ The **General settings** section defines basic properties of your service. |:----------------------|:----------| | **Service name** | Enter a name for your Agent Memory service. We recommend you use a name that describes your service's purpose. | | **Select database** | Select the Redis Cloud database to use for this service from the list. | -| **User for this service** | The [database access user]({{< relref "/operate/rc/security/access-control/data-access-control/role-based-access-control" >}}) to use for this service. Agent Memory only supports the [`default` user]({{< relref "/operate/rc/security/access-control/data-access-control/default-user" >}}) during public preview. | +| **User for this service** | The [database access user]({{< relref "/operate/rc/security/access-control/data-access-control/role-based-access-control" >}}) to use for this service. Agent Memory only supports the [`default` user]({{< relref "/operate/rc/security/access-control/data-access-control/default-user" >}}). | ### Memory configuration @@ -131,7 +130,7 @@ To add a custom memory type, select **Add type** and configure the following set | Setting name |Description| |:----------------------|:----------| -| **Name** | A unique name for the custom memory type. Must start with a letter and contain only letters, numbers, hyphens, or underscores (1–64 characters). The name must be unique within the service and cannot match a built-in memory type (`semantic`, `episodic`, `message`, or `session_summary_view`). | +| **Name** | A unique name for the custom memory type. Must start with a letter and contain only letters, numbers, hyphens, or underscores (1–64 characters). The name must be unique within the service and cannot match a built-in memory type. | | **Description** | A short description of what the memory type represents (1–200 characters). | | **Fields** | The structured fields that make up the memory type. See [Fields](#fields) below. | @@ -164,9 +163,6 @@ The **Sensitive-data exclusions** section lets you define information that shoul Detector matches are applied deterministically. Semantic exclusions only steer the extraction model, so they are advisory. -> [!NOTE] -> Sensitive-data exclusions are an early-stage feature, enabled for selected accounts. If you want to try them, contact your Redis representative or [contact sales](https://redis.io/contact/). - ![The Sensitive-data exclusions section.](/images/rc/agent-memory-sensitive-data-exclusions.png) Exclusions are applied when long-term memories are created from a session. Session memory is unaffected: events and their summaries are kept as sent. Exclusions are also not applied to long-term memories your application creates directly through the API or an SDK. @@ -245,7 +241,7 @@ If an error occurs, verify that your database is active. For help, [contact supp ## Next steps -After Redis Cloud creates your service, [continue with the REST API quickstart]({{< relref "/develop/ai/context-engine/agent-memory/rest-api-quickstart" >}}). +After Redis Cloud creates your service, [continue with the quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}). To generate a conversational agent that connects to your service, open the [AI agent builder]({{< relref "/develop/ai/agent-builder" >}}) and select **Redis Iris Conversational Assistant**. diff --git a/content/operate/iris/agent-memory/model-configuration.md b/content/operate/iris/agent-memory/model-configuration.md index 075a70da74..82dbd0c192 100644 --- a/content/operate/iris/agent-memory/model-configuration.md +++ b/content/operate/iris/agent-memory/model-configuration.md @@ -9,7 +9,6 @@ hideListLinks: true linktitle: Configure AI models title: Configure AI models for Agent Memory weight: 10 -bannerText: Redis Agent Memory on Redis Cloud is currently available as a public preview. Features and behavior are subject to change. --- You can use Redis-managed credentials for the AI models used by an Agent Memory service, or bring your own model provider API key. diff --git a/content/operate/iris/agent-memory/self-managed/_index.md b/content/operate/iris/agent-memory/self-managed/_index.md index f29b360132..1fd40fa3df 100644 --- a/content/operate/iris/agent-memory/self-managed/_index.md +++ b/content/operate/iris/agent-memory/self-managed/_index.md @@ -9,7 +9,7 @@ description: Deploy, configure, secure, and operate Redis Agent Memory on a self linkTitle: Self-managed weight: 40 hideListLinks: true -bannerText: Redis Agent Memory self-managed is currently in private preview and subject to change. A license key is required to deploy. Contact your Redis representative or [contact sales](https://redis.io/contact/). +bannerText: A license key is required to deploy. Contact your Redis representative or [contact sales](https://redis.io/contact/). bannerChildren: true aliases: - /develop/ai/context-engine/agent-memory/self-managed/ @@ -31,8 +31,8 @@ documents the self-managed admin endpoints for stores. ## What you are deploying -The `redis-agent-memory` chart `0.8.0` deploys the Data Plane, workers, and Control Plane. -Install the `iris-identity-service` chart `0.10.0` as a separate release first, then connect +The `redis-agent-memory` chart `0.10.2` deploys the Data Plane, workers, and Control Plane. +Install the `iris-identity-service` chart `0.10.2` as a separate release first, then connect Memory to it. Memory does not deploy Identity Service resources or include it as a subchart. You provide the Redis databases. Follow [Deploy](/content/operate/iris/agent-memory/self-managed/deploy.md) to configure both releases. @@ -91,4 +91,4 @@ provides searchable context across sessions. Long-term memory `memoryType` is an open identifier. When omitted on create, Redis Agent Memory stores the record as `semantic`; built-in names include `semantic`, -`episodic`, `message`, and `session_summary_view`. +`episodic`, and `message`. diff --git a/content/operate/iris/agent-memory/self-managed/api-examples.md b/content/operate/iris/agent-memory/self-managed/api-examples.md index 5f86c0f3dd..84e35ceb0a 100644 --- a/content/operate/iris/agent-memory/self-managed/api-examples.md +++ b/content/operate/iris/agent-memory/self-managed/api-examples.md @@ -5,7 +5,7 @@ categories: - docs - operate - iris -description: Use curl examples with the Redis Agent Memory self-managed Control Plane and Data Plane APIs. +description: Manage a self-managed Redis Agent Memory service and make application requests with SDKs or curl. linkTitle: Self-managed API examples weight: 70 hideListLinks: true @@ -141,6 +141,208 @@ A store may define at most 32 custom detectors. Update an existing store's polic For what each mechanism does, how a match is handled, and which memory paths exclusions apply to, see [exclude sensitive data from automatic extraction]({{< relref "/develop/ai/context-engine/agent-memory/developer-guide#exclude-sensitive-data-from-automatic-extraction" >}}). +## Manage a service with the Control Plane SDK + +The same package supplies two independently configured clients: + +| Client | Purpose | Connection and credential | +|:-------|:--------|:--------------------------| +| `AgentMemoryService` | Manage stores and extractors. Supply the store ID on each call. | Control Plane URL and operator `token`. | +| `AgentMemory` | Read and write memory within the store selected by the constructor. | Data Plane URL, Store ID, and application credential. | + +Use the self-managed Control Plane admin token or supported operator token with `AgentMemoryService`. Its SDK uses `/v2/memory` routes; the self-managed `/v1` admin API remains available. The SDK does not derive a Data Plane URL or credential from a Control Plane response. + +Set `AGENT_MEMORY_CONTROL_PLANE_URL` and `AGENT_MEMORY_SERVICE_TOKEN` separately from `AGENT_MEMORY_URL`, `STORE_ID`, and `API_KEY`. These examples read the store and check Data Plane health: + +{{< multitabs id="memory-service-sdk" tab1="Python" tab2="TypeScript" >}} + +```python +import asyncio +import os + +from redis_agent_memory import AgentMemory, AgentMemoryService + + +async def main(): + async with AgentMemoryService( + server_url=os.environ["AGENT_MEMORY_CONTROL_PLANE_URL"], + token=os.environ["AGENT_MEMORY_SERVICE_TOKEN"], + ) as service: + store = await service.get_store_async(store_id=os.environ["STORE_ID"]) + print("Store:", store.name) + async with AgentMemory( + os.environ["AGENT_MEMORY_URL"], store_id=os.environ["STORE_ID"], + api_key=os.environ["API_KEY"], + ) as memory: + print((await memory.store_health_async()).status) + + +asyncio.run(main()) +``` + +-tab-sep- + +```typescript +import { AgentMemory, AgentMemoryService } from "@redis-iris/agent-memory"; + +async function main() { + const { AGENT_MEMORY_CONTROL_PLANE_URL, AGENT_MEMORY_SERVICE_TOKEN, + AGENT_MEMORY_URL, STORE_ID, API_KEY } = process.env; + if (!AGENT_MEMORY_CONTROL_PLANE_URL || !AGENT_MEMORY_SERVICE_TOKEN || + !AGENT_MEMORY_URL || !STORE_ID || !API_KEY) { + throw new Error("Set both planes' connection values."); + } + const service = new AgentMemoryService({ + serverURL: AGENT_MEMORY_CONTROL_PLANE_URL, token: AGENT_MEMORY_SERVICE_TOKEN, + }); + console.log("Store:", (await service.getStore(STORE_ID)).name); + const memory = new AgentMemory({ serverURL: AGENT_MEMORY_URL, storeId: STORE_ID, apiKey: API_KEY }); + console.log((await memory.storeHealth()).status); +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : "Request failed"); + process.exitCode = 1; +}); +``` + +{{< /multitabs >}} + +If omitted, the Control Plane token is read from `AGENT_MEMORY_SERVICE_TOKEN`; it does not fall back to the Data Plane key. Python service models, errors, and retry helpers use `redis_agent_memory.controlplane`; Data Plane imports remain separate. Use each client's corresponding error classes. + +Store and extractor management, detectors, and memory-type reference data have Cloud and self-managed API implementations. The generated client also includes Cloud-only account database, end user license agreement (EULA), network, store-model, metrics, flush, and empty-store operations; those return `404` on-prem. The current Cloud Control Plane does not accept a programmatic credential for this SDK. Use the Cloud console for Cloud service setup rather than sending a Data Plane key or assuming a console session is a service token. + +See [Create an extractor](#create-an-extractor) for extractor administration and the [self-managed Control Plane reference](/content/operate/iris/agent-memory/self-managed/control-plane-api-reference.md) for the admin REST API. Webhook-policy configuration uses the admin REST API; it is not a method on the current service SDK. + +## Create an extractor + +For model and processing settings, see [Configure document ingestion](/content/operate/iris/agent-memory/self-managed/configuration.md#enable-document-ingestion). Set `AGENT_MEMORY_CONTROL_PLANE_URL`, `AGENT_MEMORY_SERVICE_TOKEN`, and `STORE_ID` as described in [Manage a service with the Control Plane SDK](#manage-a-service-with-the-control-plane-sdk). + +First register a custom memory type on the store. For a new `document_fact` type, run this once: + +```sh +curl --fail-with-body --silent --show-error --request PATCH \ + --header "Authorization: Bearer $AGENT_MEMORY_SERVICE_TOKEN" \ + --header 'Content-Type: application/json' \ + --data '{"addCustomMemoryTypes":[{"name":"document_fact","description":"Useful facts extracted from documents"}]}' \ + "$AGENT_MEMORY_CONTROL_PLANE_URL/v2/memory/stores/$STORE_ID" | jq +``` + +Use an existing registered type instead if it already has the fields you need. Existing types cannot be redefined by adding them again. + +Create an extractor with `source: documents` and `strategy: custom`. Save its returned `id` as `EXTRACTOR_ID`: + +{{< multitabs id="document-extractor" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python +import asyncio +import os + +from redis_agent_memory import AgentMemoryService + + +async def main(): + async with AgentMemoryService( + server_url=os.environ["AGENT_MEMORY_CONTROL_PLANE_URL"], + token=os.environ["AGENT_MEMORY_SERVICE_TOKEN"], + ) as service: + extractor = await service.create_extractor_async( + store_id=os.environ["STORE_ID"], + name="Travel guide facts", memory_type="document_fact", + source="documents", strategy="custom", + prompt="Extract useful travel facts. Keep each fact in one memory.", + ) + print(extractor.model_dump_json(by_alias=True)) + + +asyncio.run(main()) +``` + +-tab-sep- + +```typescript +import { AgentMemoryService } from "@redis-iris/agent-memory"; + +async function main() { + const { AGENT_MEMORY_CONTROL_PLANE_URL, AGENT_MEMORY_SERVICE_TOKEN, STORE_ID } = process.env; + if (!AGENT_MEMORY_CONTROL_PLANE_URL || !AGENT_MEMORY_SERVICE_TOKEN || !STORE_ID) { + throw new Error("Set the Control Plane connection values and STORE_ID."); + } + const service = new AgentMemoryService({ + serverURL: AGENT_MEMORY_CONTROL_PLANE_URL, token: AGENT_MEMORY_SERVICE_TOKEN, + }); + console.dir(await service.createExtractor(STORE_ID, { + name: "Travel guide facts", memoryType: "document_fact", + source: "documents", strategy: "custom", + prompt: "Extract useful travel facts. Keep each fact in one memory.", + }), { depth: null }); +} + +main().catch((error) => { console.error(error instanceof Error ? error.message : "Request failed"); process.exitCode = 1; }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error --request POST \ + --header "Authorization: Bearer $AGENT_MEMORY_SERVICE_TOKEN" \ + --header 'Content-Type: application/json' \ + --data '{"name":"Travel guide facts","memoryType":"document_fact","source":"documents","strategy":"custom","prompt":"Extract useful travel facts. Keep each fact in one memory."}' \ + "$AGENT_MEMORY_CONTROL_PLANE_URL/v2/memory/stores/$STORE_ID/extractors" | jq +``` + +{{< /multitabs >}} + +Set `destinationNamespaceId` when creating an extractor to place its output in one namespace in the same store. Use a stable namespace ID, not a display name. Personal destinations must be compatible with the document owner; shared placement does not make the original document accessible to every reader of the extracted memories. + +List or read extractors before editing them. Updates and removal require the current `expectedRevision`; a stale revision returns `409`. An update can change the name, prompt, enabled flag, or destination. An empty destination clears it; omission keeps it. Accepted ingestions retain their extractor snapshot, so a later edit does not change an existing run. + +After setup, applications can [submit and track documents](/content/develop/ai/context-engine/agent-memory/memory-etl.md#submit-and-track-a-document) with their Data Plane credentials. + +## Configure a webhook policy + +First [enable ingestion webhooks](/content/operate/iris/agent-memory/self-managed/configuration.md#enable-ingestion-webhooks). Use the self-managed Control Plane REST API and an operator credential. The Data Plane SDKs expose status reads; `AgentMemoryService` does not currently expose hook-policy management. Set `AGENT_MEMORY_CONTROL_PLANE_URL`, `AGENT_MEMORY_SERVICE_TOKEN`, `STORE_ID`, and `HOOK_URL`. For the receiver example, use `https://:8443/hook`, or the HTTPS ingress URL that forwards to it. + +A policy PUT replaces the whole policy. Include every phase you want to retain; omitting a phase removes it. Read the current revision before writing: + +```sh +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $AGENT_MEMORY_SERVICE_TOKEN" \ + "$AGENT_MEMORY_CONTROL_PLANE_URL/v1/stores/$STORE_ID/ingestion-hooks" > hook-config.json +``` + +A new store starts at revision 0. Generate separate 32-byte signing keys for the two phases, then configure them with delivery still disabled: + +```sh +export HOOK_INPUT_KEY="$(openssl rand -base64 32)" +export HOOK_OUTPUT_KEY="$(openssl rand -base64 32)" + +jq --arg url "$HOOK_URL" \ + '{expectedRevision:.revision,enabled:false,beforeInput:{url:$url,paths:["session.append","memory.create","document.submit"],signingKey:env.HOOK_INPUT_KEY},beforeApply:{url:$url,paths:["session.builtin","session.custom","document.chunk"],signingKey:env.HOOK_OUTPUT_KEY}}' \ + hook-config.json | curl --fail-with-body --silent --show-error --request PUT \ + --header "Authorization: Bearer $AGENT_MEMORY_SERVICE_TOKEN" \ + --header 'Content-Type: application/json' --data @- \ + "$AGENT_MEMORY_CONTROL_PLANE_URL/v1/stores/$STORE_ID/ingestion-hooks" > hook-config-new.json +``` + +The response returns each phase's `keyId` and `signingKeyConfigured`, never its key. Supply those IDs and original keys to the [receiver example](/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md#verify-callbacks-and-return-decisions) through `HOOK_KEYS_JSON`: + +```sh +export HOOK_KEYS_JSON="$(jq -c '{(.beforeInput.keyId):env.HOOK_INPUT_KEY,(.beforeApply.keyId):env.HOOK_OUTPUT_KEY}' hook-config-new.json)" +``` + +After the receiver is ready, enable the same policy using the returned revision and omitting signing keys to retain them: + +```sh +jq '{expectedRevision:.revision,enabled:true,beforeInput:{url:.beforeInput.url,paths:.beforeInput.paths},beforeApply:{url:.beforeApply.url,paths:.beforeApply.paths}}' \ + hook-config-new.json | curl --fail-with-body --silent --show-error --request PUT \ + --header "Authorization: Bearer $AGENT_MEMORY_SERVICE_TOKEN" \ + --header 'Content-Type: application/json' --data @- \ + "$AGENT_MEMORY_CONTROL_PLANE_URL/v1/stores/$STORE_ID/ingestion-hooks" | jq +``` + +A stale `expectedRevision` returns `409`. To rotate a key, supply a new `signingKey` and load its returned ID into the receiver. Retain the previous receiver key for 24 hours: pending work keeps its original endpoint and key version. Keep the previous endpoint reachable while that work completes; delivery does not follow redirects. Policy changes affect new work; they do not rewrite saved decisions or a pending run's policy. + ## Identity Service API examples Manage agent keys with the Identity Service, using the Identity Service control token. @@ -270,28 +472,30 @@ Set variables: DP_URL="http://localhost:9000" STORE_ID="" RAM_AGENT_KEY="" +OWNER_ID="" +USER_TOKEN="" ``` -For auth-disabled deployments, omit the `Authorization` header and rely on the -deployment's hosting controls. +With ownership enforcement enabled, [configure delegation](/content/operate/iris/agent-memory/self-managed/authentication.md#delegate-a-user-with-an-agent-key), set `USER_TOKEN` to the user's access token, and set `OWNER_ID` to its verified user ID. If enforcement is disabled, unset `USER_TOKEN` and enforce access in your backend. For auth-disabled deployments, also omit `Authorization` and rely on the deployment's hosting controls. ### Write a session event ```bash curl -sS -X POST "$DP_URL/v1/stores/$STORE_ID/session-memory/events" \ -H "Authorization: Bearer $RAM_AGENT_KEY" \ + -H "X-Iris-User-Token: ${USER_TOKEN:-}" \ -H "Content-Type: application/json" \ - -d '{ + -d "$(jq -n --arg owner "$OWNER_ID" --arg now "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" '{ "sessionId": "session-001", - "actorId": "user-001", + "actorId": $owner, "role": "USER", "content": [ { "text": "What is the capital of France?" } ], - "createdAt": "2026-06-25T18:00:00Z" - }' + "createdAt": $now + }')" ``` Request body fields: @@ -308,7 +512,8 @@ Request body fields: ```bash curl -sS "$DP_URL/v1/stores/$STORE_ID/session-memory/session-001" \ - -H "Authorization: Bearer $RAM_AGENT_KEY" + -H "Authorization: Bearer $RAM_AGENT_KEY" \ + -H "X-Iris-User-Token: ${USER_TOKEN:-}" ``` ### List sessions @@ -316,52 +521,96 @@ curl -sS "$DP_URL/v1/stores/$STORE_ID/session-memory/session-001" \ List sessions for a specific owner: ```bash -curl -sS "$DP_URL/v1/stores/$STORE_ID/session-memory?filterOwnerId=user-001" \ - -H "Authorization: Bearer $RAM_AGENT_KEY" +curl -sS --get "$DP_URL/v1/stores/$STORE_ID/session-memory" \ + -H "Authorization: Bearer $RAM_AGENT_KEY" \ + -H "X-Iris-User-Token: ${USER_TOKEN:-}" \ + --data-urlencode "filterOwnerId=$OWNER_ID" ``` -List all sessions: +List all sessions the caller can access: ```bash curl -sS "$DP_URL/v1/stores/$STORE_ID/session-memory?includeAll=true" \ - -H "Authorization: Bearer $RAM_AGENT_KEY" + -H "Authorization: Bearer $RAM_AGENT_KEY" \ + -H "X-Iris-User-Token: ${USER_TOKEN:-}" ``` `filterOwnerId` and `includeAll` are mutually exclusive. +The `/session-memory` list returns session IDs. To get session rows ordered by +recent activity, use `/sessions`: + +```bash +curl -sS --get "$DP_URL/v1/stores/$STORE_ID/sessions" \ + -H "Authorization: Bearer $RAM_AGENT_KEY" \ + -H "X-Iris-User-Token: ${USER_TOKEN:-}" \ + --data-urlencode "filterOwnerId=$OWNER_ID" \ + --data-urlencode 'sortBy=updatedAt' \ + --data-urlencode 'sortOrder=desc' \ + --data-urlencode 'limit=20' +``` + +Each row includes `sessionId` and, when known, `ownerId`, `createdAt`, +`updatedAt`, and `namespaceRef`. Older sessions can have no owner or timestamps. +The default order is `updatedAt` descending. You can also sort by `sessionId` +or `createdAt`, each in ascending or descending order. Send `nextPageToken` as +`pageToken` with the same filters and sort options to fetch the next page. +Both routes use the same filter and authorization rules. + ### Create long-term memories directly +Create a personal namespace for the user's preferences and save its ID. Run this once, then reuse the ID for later writes: + +```bash +NAMESPACE_ID=$(curl --fail-with-body -sS -X POST "$DP_URL/v1/stores/$STORE_ID/namespaces" \ + -H "Authorization: Bearer $RAM_AGENT_KEY" \ + -H "X-Iris-User-Token: ${USER_TOKEN:-}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg owner "$OWNER_ID" '{name:"preferences",scope:"PERSONAL",ownerId:$owner}')" \ + | jq -er '.namespace.namespaceId') +export NAMESPACE_ID +``` + +After the request succeeds, place the memory in that namespace: + ```bash curl -sS -X POST "$DP_URL/v1/stores/$STORE_ID/long-term-memory" \ -H "Authorization: Bearer $RAM_AGENT_KEY" \ + -H "X-Iris-User-Token: ${USER_TOKEN:-}" \ -H "Content-Type: application/json" \ - -d '{ + -d "$(jq -n --arg ns "$NAMESPACE_ID" --arg owner "$OWNER_ID" '{ "memories": [ { "id": "pref-email-updates", "text": "User prefers email updates.", "memoryType": "semantic", - "ownerId": "user-001", - "namespace": "preferences", + "ownerId": $owner, + "namespaceRef": {"namespaceId": $ns}, "topics": ["communications"] } ] - }' + }')" ``` +See [organize memories with namespaces]({{< relref "/develop/ai/context-engine/agent-memory/namespaces" >}}) for hierarchy, search, and management guidance. + ### Search long-term memory +Filter by the namespace ID to recall preferences from that namespace: + ```bash curl -sS -X POST "$DP_URL/v1/stores/$STORE_ID/long-term-memory/search" \ -H "Authorization: Bearer $RAM_AGENT_KEY" \ + -H "X-Iris-User-Token: ${USER_TOKEN:-}" \ -H "Content-Type: application/json" \ - -d '{ + -d "$(jq -n --arg ns "$NAMESPACE_ID" --arg owner "$OWNER_ID" '{ "text": "How should we contact this user?", "filter": { + "namespaceRef": {"eq": $ns}, "ownerId": { - "eq": "user-001" + "eq": $owner } }, "limit": 5 - }' + }')" ``` diff --git a/content/operate/iris/agent-memory/self-managed/authentication.md b/content/operate/iris/agent-memory/self-managed/authentication.md index ad2934664c..0cf6a6a62b 100644 --- a/content/operate/iris/agent-memory/self-managed/authentication.md +++ b/content/operate/iris/agent-memory/self-managed/authentication.md @@ -16,7 +16,7 @@ aliases: Self-managed Redis Agent Memory uses separate authentication models for the Control Plane and Data Plane. -The Control Plane uses an admin token for management endpoints. The Data Plane checks each agent +The Control Plane uses an admin token for management endpoints by default. It also accepts operator tokens when OpenID Connect (OIDC) authentication is configured. The Data Plane checks each agent key with the Identity Service, using the Data Plane service credential, and enforces store-level grants. @@ -24,7 +24,7 @@ grants. | Credential | Who presents it to whom | Secret / key | Auto-generated? | What it unlocks | | --- | --- | --- | --- | --- | -| Control Plane admin token | Operator → Control Plane (`:9100`) | `redis-agent-memory-controlplane-admin-token` / `token` | Yes (`controlplane.adminToken.autoGenerate`) | Store management: `/v1/stores`, `/v1/detectors` | +| Control Plane admin token | Operator → Control Plane (`:9100`) | `redis-agent-memory-controlplane-admin-token` / `token` | Yes (`controlplane.adminToken.autoGenerate`) | Store and extractor management through the `/v1` admin API and supported `/v2/memory` routes | | Control Plane internal token | Identity Service → Control Plane | `memory-internal-token` / `token` in the walkthrough | You provision it for both releases | Checking that the store in a grant exists. Set `controlplane.internalToken.existingSecret` on Memory and `productValidation.products.memory.credential.existingSecret` on Identity Service. | | Identity Service control token | Operator → Identity Service (`:9200`) | `ids-control-token` / `token` in the walkthrough | Supplied through `control.existingSecret` on Identity Service; that chart can also generate one | Creating, listing, updating, rotating and revoking agent keys | | Data Plane service credential | Data Plane → Identity Service | `memory-dp-credential` / `token` in the walkthrough | You provision it for both releases | Checking agent keys for `memory`. Set `identityService.credential.existingSecret` on Memory and register it in `runtime.serviceCredentials[]` on Identity Service. | @@ -37,7 +37,7 @@ Secret names and keys must agree between releases. See ## Control Plane admin token -Control Plane management endpoints require: +With the default configuration, Control Plane management endpoints require: ```http Authorization: Bearer @@ -87,6 +87,8 @@ Use auth-disabled mode for development and testing only: ```yaml profile: dev +ownership: + enabled: false auth: method: none ``` @@ -148,6 +150,177 @@ Operation mapping: | `write` | Add/delete session events, delete session memory, set session summary, set session property, create/update/delete long-term memory. | {{< /table-scrollable >}} +## Server-side ownership enforcement + +Store grants authorize access to a store. Ownership enforcement adds a check within that store: ordinary callers can access their own personal records and shared records. + +This section applies to on-prem deployments. Release 0.10.2 enables ownership enforcement by default in the on-prem server, worker, and chart. + +| Setting | Personal access | Shared access | +| --- | --- | --- | +| `ownership.enabled: false` | Your trusted backend must authorize every read and write, including direct-ID requests. Owner fields and filters do not authenticate the user. | Your backend checks which shared namespaces the user may access. | +| `ownership.enabled: true` | The server compares the stored owner with the authenticated user ID. Search and list restrictions apply in addition to client filters. | Callers with the required store permission can access shared records. Shared scope does not enforce team membership. | + +Keep application-specific checks in your backend in both modes. Use separate stores and grants if teams need separate server-enforced access. Worker credentials are for background services; do not give them to applications. + +### Match stored owners to authenticated users + +Use the same stable user ID in stored records and the verified user token. Matching is exact, including case. IDs must meet the format in the [API reference](/content/develop/ai/context-engine/agent-memory/api-reference.md); an email address or an identity-provider subject containing punctuation may need a separate compatible claim. Changing a claim mapping does not rewrite stored owners. + +The first session event's `actorId` becomes the session owner. Later events in an accessible session can use an assistant's actor ID without changing ownership. Directly created memories and personal namespace roots use the `ownerId` supplied by your application. Session extraction inherits the session owner. See [Identify users and conversations](/content/develop/ai/context-engine/agent-memory/developer-guide.md#identify-users-and-conversations) for client and server fields. + +With enforcement on: + +- A new personal owner, or an owner update, must match the authenticated user ID. A mismatch returns `403 Forbidden`, including when the record would enter a shared namespace. +- Another user's personal record is hidden from direct-ID reads and mutations with `404 Not Found`. Search and list filters cannot expand access. +- A personal namespace child inherits its root's owner. +- A caller without a user ID can access shared records within its grants, but cannot claim a named personal owner. Ownerless private records are hidden. + +Ordinary agent keys created by `POST /v1/api-keys` do not contain a user binding. The API does not accept one or copy the key creator's identity. Use delegation for personal access. Configuring an OpenID Connect (OIDC) claim alone does not bind a key. + +An unbound key can pass store health checks and access allowed shared data while personal creation returns `403`. A successful health check does not establish a user identity. + +### Delegate a user with an agent key + +Delegation lets an application use its agent key for store permissions and a user's access token for personal identity. This path is supported in on-prem release 0.10.2. It does not add the user's store grants to the key. + +1. Configure your identity provider to issue signed access tokens with an issuer, a compatible user ID claim, and the intended audience. Obtain the user's token through your application's login process. +1. Configure the Data Plane to trust that issuer and its JSON Web Key Set (JWKS). Use the same tenant as the agent key and store. +1. Enable delegation and choose an audience distinct from direct OIDC sign-in. +1. Send both credentials on each request for that user. + +For chart-rendered configuration, merge this fragment into your existing Helm values. Replace the issuer, JWKS URL, and audience values with those configured by your identity provider: + +```yaml +tenantId: default +auth: + oidc: + audience: api://memory-direct +memory: + ownership: + enabled: true + auth: + oidc: + enabled: true + issuer: https:/// + jwks_uri: https:/// + user_id_claim: sub + allow_azp_fallback: false + delegation: + enabled: true + audience: + - api://memory-delegation +``` + +The chart supplies the static tenant from `tenantId`. For a bring-your-own configuration file, place `ownership` and `auth` at the root, set `auth.oidc.static_tenant_id` to that same tenant, and set `auth.oidc.audience` to the direct-sign-in audience as a list. Do not also configure `tenant_id_claim` for a single-tenant deployment. + +The server verifies the token's signature, issuer, audience, expiry, tenant, and user claim. JWKS verification does not decrypt a token or exchange it for another credential. An application-issued signed JSON Web Token (JWT) works only when the deployment trusts its issuer and verification keys. + +Delegation defaults to off and requires OIDC to be enabled. Its audience must not overlap the direct OIDC audience; keep `allow_azp_fallback` off with a separate delegation audience. Without a separate audience, the same user token can also be presented as a direct sign-in. + +Direct OIDC sign-in remains a preview in this release: it validates the token but does not resolve the subject's store grants. A valid token alone receives `403` for store operations. Use the agent key and delegated token together for personal access. + +Set `USER_TOKEN` to the access token for the current request's user, and `OWNER_ID` to that token's verified user ID. Use the connection values from the [quickstart](/content/develop/ai/context-engine/agent-memory/quickstart.md). These examples list that user's session metadata: + +{{< multitabs id="delegated-user" tab1="Python" tab2="TypeScript" tab3="curl" >}} + +```python +import asyncio +import os + +import httpx +from redis_agent_memory import AgentMemory + + +async def main(): + async with httpx.AsyncClient( + headers={"X-Iris-User-Token": os.environ["USER_TOKEN"]}, + ) as transport: + async with AgentMemory( + server_url=os.environ["AGENT_MEMORY_URL"], + store_id=os.environ["STORE_ID"], + api_key=os.environ["API_KEY"], + async_client=transport, + ) as memory: + page = await memory.list_session_summaries_async( + filter_owner_id=os.environ["OWNER_ID"], limit=20, + ) + print(page.model_dump_json(by_alias=True)) + + +asyncio.run(main()) +``` + +-tab-sep- + +```typescript +import { AgentMemory } from "@redis-iris/agent-memory"; +import { HTTPClient } from "@redis-iris/agent-memory/lib/http"; + +async function main() { + const { AGENT_MEMORY_URL, STORE_ID, API_KEY, USER_TOKEN, OWNER_ID } = process.env; + if (!AGENT_MEMORY_URL || !STORE_ID || !API_KEY || !USER_TOKEN || !OWNER_ID) { + throw new Error("Set the connection values, USER_TOKEN, and OWNER_ID."); + } + const httpClient = new HTTPClient(); + httpClient.addHook("beforeRequest", (request) => { + request.headers.set("X-Iris-User-Token", USER_TOKEN); + return request; + }); + const memory = new AgentMemory({ + serverURL: AGENT_MEMORY_URL, storeId: STORE_ID, apiKey: API_KEY, httpClient, + }); + console.dir(await memory.listSessionSummaries(20, undefined, OWNER_ID), { depth: null }); +} + +main().catch((error) => { console.error(error instanceof Error ? error.message : "Request failed"); process.exitCode = 1; }); +``` + +-tab-sep- + +```sh +curl --fail-with-body --silent --show-error --get \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: $USER_TOKEN" \ + --data-urlencode "filterOwnerId=$OWNER_ID" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/sessions" | jq +``` + +{{< /multitabs >}} + +Create a separate request-scoped client or transport for each user's token. Do not change a shared client's user header while it handles concurrent requests. + +A missing or invalid primary credential, an invalid or expired user token, or a missing user identity returns `401`. A token from another tenant, or a delegation header used with a credential other than an agent key, returns `403`. A valid delegated user still needs the key's store grant. + +### Enable or change ownership enforcement + +For a new deployment, configure the user-identity path before writing personal data. If your application does not use a trusted identity provider, explicitly disable enforcement and apply [backend authorization](/content/develop/ai/context-engine/agent-memory/developer-guide.md#authorize-every-memory-request). + +Before upgrading an existing deployment: + +1. Keep `memory.ownership.enabled: false` explicitly set while preparing the transition. +1. Compare stored personal owners with the verified user IDs your application will send. Resolve owner mismatches before enabling enforcement; the setting does not rewrite owners. +1. Configure delegation, then set `memory.ownership.enabled: true` in chart-rendered values. For a bring-your-own file, use `ownership.enabled: true` at the root. +1. Apply the setting to both Data Plane servers and workers, then roll out those processes. `MEM_OWNERSHIP_ENABLED` overrides the file value; check both for conflicting settings. +1. Replace existing Summary Views when changing this setting. A view retains the source-access policy from its creation. +1. Test two users: each can access their own personal records, neither can retrieve or search for the other's records, and both can access shared records allowed by their store grants. +1. Test an agent key without a user token: it must not retrieve personal records or create a session for a named owner. + +For the two-user read check, use an ordinary agent key with store read access, without operator privileges. Set `USER_TOKEN` to the first user's valid token. Set `OWN_SESSION_ID`, `OTHER_SESSION_ID`, and `SHARED_SESSION_ID` to existing sessions owned by that user, owned by the other user, and placed in a shared namespace, respectively: + +```sh +for session_id in "$OWN_SESSION_ID" "$OTHER_SESSION_ID" "$SHARED_SESSION_ID"; do + curl --silent --show-error --output /dev/null --write-out '%{http_code}\n' \ + --header "Authorization: Bearer $API_KEY" \ + --header "X-Iris-User-Token: $USER_TOKEN" \ + "$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/$session_id" +done +``` + +Expect `200`, `404`, and `200`, in that order. Repeat with the second user's token and swap `OWN_SESSION_ID` with `OTHER_SESSION_ID`. A `401` indicates a token configuration problem; it does not prove ownership enforcement works. + +Disabling enforcement restores store-wide access within the credential's grants. Keep backend authorization in place before disabling it. It does not remove stored owner fields, reassign records, or remove the need for store authentication. + ## Worker callbacks Redis Agent Memory workers consume background jobs and call the Data Plane to read diff --git a/content/operate/iris/agent-memory/self-managed/configuration.md b/content/operate/iris/agent-memory/self-managed/configuration.md index b0fd76882b..d1c05e0915 100644 --- a/content/operate/iris/agent-memory/self-managed/configuration.md +++ b/content/operate/iris/agent-memory/self-managed/configuration.md @@ -43,6 +43,7 @@ Data Plane settings go under `memory`, or at the top level of a BYO Data Plane c | `databases` | Required. Store Redis, as `databases."1".urls`. | | `embedding.models.default_embedding_model`, `embedding.models.dimensions` | Required. The embedding model and its vector size. The Control Plane's `embedding.dimensions` must match. | | `auth` | Data Plane authentication. Defaults to `agent_key`. | +| `ownership.enabled` | Restricts personal records to their authenticated owner while allowing shared records. Defaults to `true` in on-prem release 0.10.2. Requires a matching authenticated user ID for personal access; use an agent key with a delegated user token, or explicitly disable enforcement and authorize requests in your backend. See [Server-side ownership enforcement](/content/operate/iris/agent-memory/self-managed/authentication.md#server-side-ownership-enforcement) before enabling it. | | `inference_providers` | Required when any `llm` block is set. Each entry holds a provider's `endpoint`. | | `server` | Data Plane bind address and port. | | `license.license_path` | Path where the license Secret is mounted. | @@ -69,6 +70,8 @@ The runtime endpoint must use `https://`. An `http://` endpoint is accepted only Subjects using the `identity_service` authority also need this connection for grant lookup, even when another authentication method is selected. +For ownership enforcement, use `memory.ownership.enabled` in Helm values or `ownership.enabled` in a BYO Data Plane configuration file. `MEM_OWNERSHIP_ENABLED` overrides the file setting. Enabling this option does not bind agent keys to users or rewrite existing owners. + ## Bring-your-own (BYO) configuration With BYO configuration, you supply each complete configuration file in your own Secret, and the @@ -289,3 +292,160 @@ subjectGrants: Also add `subject-grant-lookup` to the `memory-dp` runtime credential's `allowedOperations` alongside `api-key-introspect`. These are Identity Service values, not Memory's `identityService` values. `read_all_owners` is a subject-grant action; do not add it to agent-key grants. + +## Configure document ingestion {#enable-document-ingestion} + +Document ingestion is enabled in self-managed deployments. Configure the server and worker together through chart-rendered values. This example sets explicit processing limits: + +```yaml +memory: + document_ingestion: + enabled: true + max_decoded_bytes: 1048576 + max_input_tokens: 8000 + max_chunks: 32 + max_output_records: 100 + max_queued_bytes_per_store: 10485760 + max_retries: 3 + max_execution_seconds: 900 + source_retention_seconds: 86400 + checkpoint_retention_seconds: 86400 + background_jobs: + redis: + enabled: true + jobs: + document-ingestion: + concurrency: 1 + timeout: 15m + max_retry: 3 + promote_session_memory: + strategies: + custom: + llm: + provider: openai + models: + default_chat_model: + credentials: + type: static + api_key: +``` + +Use a model supported by your configured `inference_providers.openai` connection. Supply the credential through the existing overlay Secret. The custom extraction model is required even if built-in session extraction already has a model. + +For a bring-your-own configuration, place these fields at the file root instead of under `memory`. Keep the worker's queue retries and timeout consistent with the ingestion limits. All numeric ingestion limits must be positive. Byte, estimated token, and chunk limits apply together. + +Roll out both the server and worker. Confirm store health, then [create an extractor and submit a document](/content/develop/ai/context-engine/agent-memory/memory-etl.md). Setting `enabled: false` rejects new ingestions; it does not undo memories already written. + +## Configure Summary Views {#enable-summary-views} + +Summary Views are enabled in self-managed deployments. Configure the server and worker with the same ownership setting, model provider, and explicit bounds. For chart-rendered configuration, merge these example values under `memory`: + +```yaml +memory: + summary_views: + enabled: true + llm: + provider: openai + models: + default_chat_model: + credentials: + type: static + api_key: + page_size: 10 + max_definitions: 100 + max_partitions: 100 + max_concurrent_refreshes: 2 + max_concurrent_refreshes_per_store: 1 + max_input_tokens: 8000 + max_source_records: 100 + max_source_tokens: 100000 + default_debounce: 1s + min_debounce: 1s + max_debounce: 1m + default_reconcile_interval: 1h + min_reconcile_interval: 1s + max_reconcile_interval: 24h + schedule_repair_interval: 1m + cleanup_batch_size: 100 + cleanup_timeout: 5s + generation: + max_output_tokens: 2000 + timeout: 30s +``` + +Use the deployment's configured model-provider connection and supply its credential through the overlay Secret. The selected provider must support the required text or structured generation; the OpenAI connection uses `chat_completions` for structured output. These numbers are examples. Required bounds have no defaults when Summary Views are enabled; set them explicitly and validate them under your expected load. + +| Bounds | How to choose them | +|:-------|:-------------------| +| `max_input_tokens`, `generation.max_output_tokens` | Leave room for the prompt and output within the selected model’s context limit. | +| `max_source_records`, `max_source_tokens`, `max_partitions`, `max_definitions` | Bound the amount of data each store can summarize and retain. Test representative source sizes and grouping values. | +| `max_concurrent_refreshes`, `max_concurrent_refreshes_per_store` | Budget model-provider requests and Redis load. Both caps apply per worker replica; multiply by the replica count when sizing capacity. | +| Debounce and reconciliation intervals | Balance result freshness against repeated generation cost. | +| `generation.timeout`, `cleanup_timeout` | Allow measured generation and cleanup time while bounding stalled work. | +| `page_size`, `cleanup_batch_size`, `schedule_repair_interval` | Test scan and cleanup throughput against Redis capacity. | + +With a bring-your-own file, place `summary_views` at the file root. Keep the Redis job backend, authenticated worker callbacks, and scheduling worker enabled. Roll out server and worker together, then [create and refresh a view](/content/develop/ai/context-engine/agent-memory/summary-views.md). A private view needs an authenticated user; an ordinary agent key alone does not supply one. + +Each view retains its source-access policy. Replace existing definitions when changing ownership enforcement rather than expecting a flag change to rewrite them. + +## Enable ingestion webhooks + +Start with the [self-managed deployment](/content/operate/iris/agent-memory/self-managed/deploy.md). These are process settings; a client cannot enable them through a request header. + +An operator must configure both Data Plane servers and workers: + +1. Enable `memory.webhooks.enabled` in chart-rendered configuration, or `webhooks.enabled` at the root of a bring-your-own file. +1. Enable the Redis job backend and [authenticated worker callbacks](/content/operate/iris/agent-memory/self-managed/authentication.md#worker-callbacks). +1. Configure the same signing-secret backend and prefix in the Control Plane, Data Plane, and worker. Supported backends are Vault and AWS Secrets Manager. The Control Plane writes keys; Data Plane processes read them. +1. Serve the receiver over HTTPS with a certificate trusted by the Data Plane. +1. For a private receiver address, allow its range in `memory.webhooks.private_cidrs` on both servers and workers. + +For example, allow one private receiver address in Helm values: + +```yaml +memory: + webhooks: + enabled: true + private_cidrs: + - /32 +``` + +Delivery checks all resolved addresses at connection time. Redirects and environment HTTP proxies are not used. An allowlist cannot permit loopback, link-local, metadata-service, multicast, or other protected addresses. A localhost URL is not a production callback endpoint. + +For Vault, use matching application configuration in the Data Plane and Control Plane: + +```yaml +secrets: + provider: vault +vault: + address: https://:8200 + mount: secret + base_path: agent-memory +``` + +In chart-rendered values, put this under `memory` and `controlplane.configData`, respectively. Supply `vault.token` through a mounted configuration Secret. The Control Plane needs permission to write and delete signing-key versions; servers and workers need read permission. Keep real Vault tokens and callback keys out of source control. + +Before enabling output hooks on an existing deployment, upgrade workers first, then servers, and drain jobs queued by the older release. Keep hooks disabled until both sides support them. Setting the deployment flag to false stops delivery and fails selected work closed; it does not approve pending operations. + +For Active-Active backing databases, route every server and worker for a store through the same authoritative writer connection. Do not mix member addresses or use region-dependent aliases for that connection. Before changing writers, drain old writers, wait for replication, and restart the processes to clear old connections. A store with hook history fails closed if its persisted writer connection is missing or invalid; regional queue ownership does not make independent writers safe. + +Then [configure a store policy](/content/operate/iris/agent-memory/self-managed/api-examples.md#configure-a-webhook-policy) and deploy a [callback receiver](/content/develop/ai/context-engine/agent-memory/ingestion-webhooks.md#verify-callbacks-and-return-decisions). + +## Configure namespace routing {#enable-namespace-routing} + +Namespace routing is enabled in self-managed deployments. Keep these switches consistent on the server and worker. For chart-rendered configuration, use: + +```yaml +memory: + namespace_features: + api: true + routing: + policy_api: true + automatic: true + admission: true + strategies: [matcher_spec] +``` + +For a bring-your-own file, put `namespace_features` at the file root. This example enables the `matcher_spec` strategy used by the regex policy in the routing guide. + +After rollout, use the read-policy request in [Configure a policy](/content/develop/ai/context-engine/agent-memory/namespaces/namespace-routing.md#configure-a-policy). A successful response confirms that the policy API is available. Read the returned `policy.enabled` value; saving a policy with `enabled: true` enables it for that store. API availability alone does not prove that automatic routing or admission is enabled, so check the process settings as well. diff --git a/content/operate/iris/agent-memory/self-managed/control-plane-api-reference/openapi-control-plane.json b/content/operate/iris/agent-memory/self-managed/control-plane-api-reference/openapi-control-plane.json index e48b196d40..88849785f9 100644 --- a/content/operate/iris/agent-memory/self-managed/control-plane-api-reference/openapi-control-plane.json +++ b/content/operate/iris/agent-memory/self-managed/control-plane-api-reference/openapi-control-plane.json @@ -3,7 +3,7 @@ "info": { "title": "Redis Agent Memory Control Plane API", "version": "1.0.0", - "description": "Admin API for the Redis Agent Memory self-managed Control Plane.\n\nAuthentication: every endpoint requires a single static operator token, sent by default as\n`Authorization: Bearer `. The header and scheme are configurable with\n`auth.admin_token.header` and `auth.admin_token.scheme`; the security scheme shown is the\ndefault. This token is distinct from the agent keys used by the Data Plane. Operational\nendpoints (health, build-info) are served separately and are unauthenticated.\n\nThere is no per-store database, subscription, or endpoint binding: self-managed deployments use\na single shared store database. Embedding configuration is resolved server-side from deployment\nconfig rather than supplied here.\n" + "description": "Admin API for the Redis Agent Memory self-managed Control Plane.\n\nAuthentication: management endpoints require an operator credential. By default, use a static admin token, sent as\n`Authorization: Bearer `. The header and scheme are configurable with\n`auth.admin_token.header` and `auth.admin_token.scheme`; the security scheme shown is the\ndefault. Configured OIDC operator tokens are also accepted. The admin token is distinct from the agent keys used by the Data Plane. Operational\nendpoints (health, build-info) are served separately and are unauthenticated.\n\nThere is no per-store database, subscription, or endpoint binding: self-managed deployments use\na single shared store database. Embedding configuration is resolved server-side from deployment\nconfig rather than supplied here.\n" }, "paths": { "/v1/detectors": { @@ -228,6 +228,640 @@ } } }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "GetStore", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetStore 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetStoreResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "operationId": "UpdateStore", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateStoreRequestContent" + } + } + } + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateStore 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateStoreResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/stores/{storeId}/extractors": { + "get": { + "operationId": "ListExtractors", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "ListExtractors 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListExtractorsResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "CreateExtractor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateExtractorRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "CreateExtractor 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateExtractorResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/stores/{storeId}/extractors/{extractorId}": { + "delete": { + "operationId": "RemoveExtractor", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "required": true + }, + { + "name": "extractorId", + "in": "path", + "description": "Store-scoped Extractor identifier.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store-scoped Extractor identifier." + }, + "required": true + }, + { + "name": "expectedRevision", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "RemoveExtractor 204 response" + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "GetExtractor", + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "required": true + }, + { + "name": "extractorId", + "in": "path", + "description": "Store-scoped Extractor identifier.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store-scoped Extractor identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetExtractor 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetExtractorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "operationId": "UpdateExtractor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateExtractorRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "storeId", + "in": "path", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "required": true + }, + { + "name": "extractorId", + "in": "path", + "description": "Store-scoped Extractor identifier.", + "schema": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store-scoped Extractor identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateExtractor 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateExtractorResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, "500": { "description": "UnexpectedError 500 response", "content": { @@ -239,9 +873,11 @@ } } } - }, + } + }, + "/v1/stores/{storeId}/ingestion-hooks": { "get": { - "operationId": "GetStore", + "operationId": "GetIngestionHooks", "parameters": [ { "name": "storeId", @@ -259,11 +895,11 @@ ], "responses": { "200": { - "description": "GetStore 200 response", + "description": "GetIngestionHooks 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetStoreResponseContent" + "$ref": "#/components/schemas/GetIngestionHooksResponseContent" } } } @@ -298,6 +934,16 @@ } } }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, "500": { "description": "UnexpectedError 500 response", "content": { @@ -310,16 +956,17 @@ } } }, - "patch": { - "operationId": "UpdateStore", + "put": { + "operationId": "PutIngestionHooks", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateStoreRequestContent" + "$ref": "#/components/schemas/PutIngestionHooksRequestContent" } } - } + }, + "required": true }, "parameters": [ { @@ -338,11 +985,11 @@ ], "responses": { "200": { - "description": "UpdateStore 200 response", + "description": "PutIngestionHooks 200 response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateStoreResponseContent" + "$ref": "#/components/schemas/PutIngestionHooksResponseContent" } } } @@ -387,6 +1034,26 @@ } } }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, "500": { "description": "UnexpectedError 500 response", "content": { @@ -502,7 +1169,7 @@ }, "description": { "type": "string", - "description": "What the detector matches, and what it deliberately does not — the catalog is\nprecision-first, so an operator choosing between detectors needs the\nlimitation as much as the capability." + "description": "What the detector matches, and what it deliberately does not \u2014 the catalog is\nprecision-first, so an operator choosing between detectors needs the\nlimitation as much as the capability." } }, "required": [ @@ -529,6 +1196,157 @@ "enabled" ] }, + "ConflictErrorResponseContent": { + "type": "object", + "description": "The request conflicts with the current state of the resource.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 409, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ConflictErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "ConflictErrorType": { + "type": "string", + "description": "Problem type URI for conflict errors.", + "enum": [ + "/errors/conflict" + ] + }, + "CreateExtractorRequestContent": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Customer-facing Extractor display name." + }, + "source": { + "$ref": "#/components/schemas/ExtractorSource" + }, + "strategy": { + "$ref": "#/components/schemas/ExtractorStrategy" + }, + "memoryType": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Name of a custom memory type or field (1-64 chars). Must start with a letter,\nfollowed by letters, digits, underscores, or dashes." + }, + "prompt": { + "type": "string" + }, + "destinationNamespaceId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "memoryType", + "name", + "prompt", + "source", + "strategy" + ] + }, + "CreateExtractorResponseContent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store-scoped Extractor identifier." + }, + "revision": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Customer-facing Extractor display name." + }, + "source": { + "$ref": "#/components/schemas/ExtractorSource" + }, + "strategy": { + "$ref": "#/components/schemas/ExtractorStrategy" + }, + "memoryType": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Name of a custom memory type or field (1-64 chars). Must start with a letter,\nfollowed by letters, digits, underscores, or dashes." + }, + "prompt": { + "type": "string" + }, + "destinationNamespaceId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "enabled": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "enabled", + "id", + "memoryType", + "name", + "prompt", + "revision", + "source", + "strategy", + "updatedAt" + ] + }, "CreateStoreRequestContent": { "type": "object", "properties": { @@ -695,7 +1513,7 @@ "maxLength": 64, "minLength": 1, "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", - "description": "Unique type name within the store. Must not collide with built-in types\n(semantic, episodic, message, session_summary_view)." + "description": "Unique type name within the store. Must not collide with built-in memory types." }, "description": { "type": "string", @@ -737,33 +1555,113 @@ "id" ] }, - "ExclusionAction": { + "ExclusionAction": { + "type": "string", + "enum": [ + "redact", + "drop" + ] + }, + "ExtractionCadenceConfig": { + "type": "object", + "description": "Cadence configuration for the extraction pipeline.", + "properties": { + "activeIntervalSeconds": { + "type": "integer", + "maximum": 600, + "minimum": 60, + "description": "Interval in seconds between extraction runs while a session is active (60-600).", + "format": "int32" + } + }, + "required": [ + "activeIntervalSeconds" + ] + }, + "ExtractionStrategy": { + "type": "string", + "enum": [ + "instruct" + ] + }, + "ExtractorOutput": { + "type": "object", + "properties": { + "id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store-scoped Extractor identifier." + }, + "revision": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Customer-facing Extractor display name." + }, + "source": { + "$ref": "#/components/schemas/ExtractorSource" + }, + "strategy": { + "$ref": "#/components/schemas/ExtractorStrategy" + }, + "memoryType": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Name of a custom memory type or field (1-64 chars). Must start with a letter,\nfollowed by letters, digits, underscores, or dashes." + }, + "prompt": { + "type": "string" + }, + "destinationNamespaceId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "enabled": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "enabled", + "id", + "memoryType", + "name", + "prompt", + "revision", + "source", + "strategy", + "updatedAt" + ] + }, + "ExtractorSource": { "type": "string", "enum": [ - "redact", - "drop" - ] - }, - "ExtractionCadenceConfig": { - "type": "object", - "description": "Cadence configuration for the extraction pipeline.", - "properties": { - "activeIntervalSeconds": { - "type": "integer", - "maximum": 600, - "minimum": 60, - "description": "Interval in seconds between extraction runs while a session is active (60-600).", - "format": "int32" - } - }, - "required": [ - "activeIntervalSeconds" + "documents" ] }, - "ExtractionStrategy": { + "ExtractorStrategy": { "type": "string", "enum": [ - "instruct" + "custom" ] }, "FailedDependencyErrorResponseContent": { @@ -868,6 +1766,96 @@ "/errors/insufficient-permissions" ] }, + "GetExtractorResponseContent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store-scoped Extractor identifier." + }, + "revision": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Customer-facing Extractor display name." + }, + "source": { + "$ref": "#/components/schemas/ExtractorSource" + }, + "strategy": { + "$ref": "#/components/schemas/ExtractorStrategy" + }, + "memoryType": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Name of a custom memory type or field (1-64 chars). Must start with a letter,\nfollowed by letters, digits, underscores, or dashes." + }, + "prompt": { + "type": "string" + }, + "destinationNamespaceId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "enabled": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "enabled", + "id", + "memoryType", + "name", + "prompt", + "revision", + "source", + "strategy", + "updatedAt" + ] + }, + "GetIngestionHooksResponseContent": { + "type": "object", + "properties": { + "revision": { + "type": "integer", + "format": "int64" + }, + "enabled": { + "type": "boolean" + }, + "beforeInput": { + "$ref": "#/components/schemas/InputHookEndpointOutput" + }, + "beforeApply": { + "$ref": "#/components/schemas/OutputHookEndpointOutput" + } + }, + "required": [ + "enabled", + "revision" + ] + }, "GetStoreResponseContent": { "type": "object", "properties": { @@ -920,6 +1908,82 @@ "storeId" ] }, + "InputHookEndpointOutput": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "paths": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InputHookPath" + }, + "maxItems": 10, + "minItems": 1, + "uniqueItems": true + }, + "keyId": { + "type": "string" + }, + "signingKeyConfigured": { + "type": "boolean" + } + }, + "required": [ + "keyId", + "paths", + "signingKeyConfigured", + "url" + ] + }, + "InputHookEndpointWrite": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "paths": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InputHookPath" + }, + "maxItems": 10, + "minItems": 1, + "uniqueItems": true + }, + "signingKey": { + "type": "string", + "maxLength": 44, + "minLength": 44, + "description": "Omit to keep this phase's current key. Required for a new phase.", + "format": "password" + } + }, + "required": [ + "paths", + "url" + ] + }, + "InputHookPath": { + "type": "string", + "enum": [ + "session.append", + "memory.create", + "memory.update", + "memory.delete", + "memory.move", + "session.summary", + "session.property", + "session.delete", + "session.events.delete", + "document.submit" + ] + }, "ListBuiltInDetectorsResponseContent": { "type": "object", "properties": { @@ -940,6 +2004,20 @@ "detectors" ] }, + "ListExtractorsResponseContent": { + "type": "object", + "properties": { + "extractors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExtractorOutput" + } + } + }, + "required": [ + "extractors" + ] + }, "ListStoresResponseContent": { "type": "object", "properties": { @@ -1068,9 +2146,125 @@ "/errors/resource-not-found" ] }, + "OutputHookEndpointOutput": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "paths": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OutputHookPath" + }, + "maxItems": 4, + "minItems": 1, + "uniqueItems": true + }, + "keyId": { + "type": "string" + }, + "signingKeyConfigured": { + "type": "boolean" + } + }, + "required": [ + "keyId", + "paths", + "signingKeyConfigured", + "url" + ] + }, + "OutputHookEndpointWrite": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "paths": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OutputHookPath" + }, + "maxItems": 4, + "minItems": 1, + "uniqueItems": true + }, + "signingKey": { + "type": "string", + "maxLength": 44, + "minLength": 44, + "description": "Exactly 32 bytes in canonical padded base64. Never returned by a read.", + "format": "password" + } + }, + "required": [ + "paths", + "url" + ] + }, + "OutputHookPath": { + "type": "string", + "enum": [ + "document.chunk", + "session.builtin", + "session.custom", + "summary_view.refresh" + ] + }, + "PutIngestionHooksRequestContent": { + "type": "object", + "properties": { + "expectedRevision": { + "type": "integer", + "minimum": 0, + "description": "Zero configures a store for the first time. Each successful write advances revision.", + "format": "int64" + }, + "enabled": { + "type": "boolean" + }, + "beforeInput": { + "$ref": "#/components/schemas/InputHookEndpointWrite" + }, + "beforeApply": { + "$ref": "#/components/schemas/OutputHookEndpointWrite" + } + }, + "required": [ + "enabled", + "expectedRevision" + ] + }, + "PutIngestionHooksResponseContent": { + "type": "object", + "properties": { + "revision": { + "type": "integer", + "format": "int64" + }, + "enabled": { + "type": "boolean" + }, + "beforeInput": { + "$ref": "#/components/schemas/InputHookEndpointOutput" + }, + "beforeApply": { + "$ref": "#/components/schemas/OutputHookEndpointOutput" + } + }, + "required": [ + "enabled", + "revision" + ] + }, "RegexSpec": { "type": "object", - "description": "A regular expression, evaluated by RE2 — linear-time, and therefore without\nlookaround. Rejected on write if it does not compile, or if it can match without\nconsuming text.", + "description": "A regular expression, evaluated by RE2 \u2014 linear-time, and therefore without\nlookaround. Rejected on write if it does not compile, or if it can match without\nconsuming text.", "properties": { "pattern": { "type": "string", @@ -1251,6 +2445,102 @@ "/errors/unexpected-error" ] }, + "UpdateExtractorRequestContent": { + "type": "object", + "properties": { + "expectedRevision": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Customer-facing Extractor display name." + }, + "prompt": { + "type": "string" + }, + "destinationNamespaceId": { + "type": "string", + "description": "Empty string clears the destination namespace. Omitted leaves it unchanged.\nTyped as String so a clear is not rejected by StoreId @length(min: 1)." + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "expectedRevision" + ] + }, + "UpdateExtractorResponseContent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store-scoped Extractor identifier." + }, + "revision": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Customer-facing Extractor display name." + }, + "source": { + "$ref": "#/components/schemas/ExtractorSource" + }, + "strategy": { + "$ref": "#/components/schemas/ExtractorStrategy" + }, + "memoryType": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "description": "Name of a custom memory type or field (1-64 chars). Must start with a letter,\nfollowed by letters, digits, underscores, or dashes." + }, + "prompt": { + "type": "string" + }, + "destinationNamespaceId": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-]+$", + "description": "Store identifier (1-64 chars, alphanumeric and dashes). Generated IDs are 32-char UUIDs without dashes." + }, + "enabled": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "enabled", + "id", + "memoryType", + "name", + "prompt", + "revision", + "source", + "strategy", + "updatedAt" + ] + }, "UpdateStoreRequestContent": { "type": "object", "properties": { diff --git a/content/operate/iris/agent-memory/self-managed/deploy.md b/content/operate/iris/agent-memory/self-managed/deploy.md index cbbf9b3c71..fb1f117078 100644 --- a/content/operate/iris/agent-memory/self-managed/deploy.md +++ b/content/operate/iris/agent-memory/self-managed/deploy.md @@ -22,7 +22,7 @@ These steps install Redis Agent Memory with chart-rendered configuration and age authentication, and end with one authenticated call to the Data Plane. Install Identity Service as release `ids`, then Memory as release `redis-agent-memory`. -These steps use Memory chart `0.8.0` and Identity Service chart `0.10.0` in one namespace. +These steps use Memory chart `0.10.2` and Identity Service chart `0.10.2` in one namespace. You create the shared credentials and Identity Service control token; Memory generates its Control Plane admin token. See [Credentials](/content/operate/iris/agent-memory/self-managed/authentication.md#credentials). @@ -33,6 +33,8 @@ placeholders with addresses reachable from the calling pods. Obtain their certif (CA) certificates as a PEM bundle named `service-ca.crt`; use your organization's trust bundle if the endpoints use public certificates. +Ownership enforcement is on by default. Before using an agent key with personal memories, [configure delegated user identity](/content/operate/iris/agent-memory/self-managed/authentication.md#delegate-a-user-with-an-agent-key). Applications that authorize users entirely in their backend must explicitly disable ownership enforcement in their Helm values. + Before you begin, review the [prerequisites](/content/operate/iris/agent-memory/self-managed/prerequisites.md). ### Isolated lab without TLS @@ -158,7 +160,7 @@ tls: helm repo add redis-ai https://helm.redis.io/ai helm repo update redis-ai helm install ids redis-ai/iris-identity-service \ - --version 0.10.0 \ + --version 0.10.2 \ --namespace \ -f values-ids.yaml ``` @@ -304,15 +306,13 @@ memory: - write session_summarisation: enabled: false - session_summary_view: - enabled: false image: - tag: "0.8.0" + tag: "0.10.2" controlplane: internalToken: existingSecret: memory-internal-token image: - tag: "0.8.0" + tag: "0.10.2" config: render: true configData: @@ -347,9 +347,8 @@ tls: `/etc/identity-service/runtime/memory-dp/token`. Do not hand-set the introspection endpoint or credential under `memory.auth` or `shared.auth`. - `tenantId: default` matches `runtime.apiKeyTenant.singleTenantID` in `values-ids.yaml`. The Control Plane creates admin-token stores in this tenant. -- The published Memory chart defaults `image.tag` and `controlplane.image.tag` to `0.8.0`; - the example pins them explicitly. Identity Service uses its own chart's application version, - `0.10.0`, independently. +- Both charts use application version `0.10.2`. The example pins Memory's Data Plane + and Control Plane images to that version. {{< note >}} No external model provider? To try the API with local models, see [Evaluate locally](#evaluate-locally). @@ -377,7 +376,7 @@ its row. The rows are checklist items CL-1 to CL-6. helm repo add redis-ai https://helm.redis.io/ai helm repo update redis-ai helm install redis-agent-memory redis-ai/redis-agent-memory \ - --version 0.8.0 \ + --version 0.10.2 \ --namespace \ -f ram-values.yaml kubectl -n get pods -w @@ -389,7 +388,7 @@ If you want Helm to wait, set an explicit timeout that matches the environment: ```bash helm install redis-agent-memory redis-ai/redis-agent-memory \ - --version 0.8.0 \ + --version 0.10.2 \ --namespace \ -f ram-values.yaml \ --wait \ @@ -474,21 +473,11 @@ The response's `token` is the agent key. It is shown only once. ## 8. Make one authenticated call ```bash -curl -sS -X POST "http://localhost:9000/v1/stores//session-memory/events" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "sessionId": "session-001", - "actorId": "user-001", - "role": "USER", - "content": [{"text": "What is the capital of France?"}], - "createdAt": "2026-06-25T18:00:00Z" - }' +curl --fail-with-body -sS "http://localhost:9000/v1/stores//health" \ + -H "Authorization: Bearer " ``` -The first long-term memories appear up to 5 minutes after the session events, because promotion -runs at the end of a clock-aligned 300-second window. To shorten the wait, set -`extractionCadence.activeIntervalSeconds` (60–600) on the store. No API triggers promotion. +Check that the store and its memory features are healthy. This verifies the agent key's store access without requiring a user identity. To write personal data, follow the [Data Plane examples](/content/operate/iris/agent-memory/self-managed/api-examples.md#data-plane-api-examples), including delegated user setup. ## Evaluate locally diff --git a/content/operate/iris/agent-memory/self-managed/operations.md b/content/operate/iris/agent-memory/self-managed/operations.md index 585e6bef46..ee6380f736 100644 --- a/content/operate/iris/agent-memory/self-managed/operations.md +++ b/content/operate/iris/agent-memory/self-managed/operations.md @@ -82,12 +82,12 @@ kubectl -n create secret generic memory-dp-credential \ RUNTIME_CREDENTIAL_CHECKSUM="$(shasum -a 256 ./memory-dp-token | awk '{print $1}')" helm upgrade ids redis-ai/iris-identity-service \ - --version 0.10.0 \ + --version 0.10.2 \ --namespace \ -f values-ids.yaml \ --set-string "runtime.serviceCredentials[0].existingSecretChecksum=$RUNTIME_CREDENTIAL_CHECKSUM" helm upgrade redis-agent-memory redis-ai/redis-agent-memory \ - --version 0.8.0 \ + --version 0.10.2 \ --namespace \ -f ram-values.yaml \ --set-string "identityService.credential.existingSecretChecksum=$RUNTIME_CREDENTIAL_CHECKSUM" @@ -174,8 +174,8 @@ kubectl -n rollout restart deployment Memory and Identity Service have independent release versions. Upgrade each with its own values file and verify their shared connection contract afterward. A Memory upgrade does not -upgrade Identity Service. The published pair documented here is Memory `0.8.0` and Identity -Service `0.10.0`. +upgrade Identity Service. The published pair documented here is Memory `0.10.2` and Identity +Service `0.10.2`. For updates within a supported release path: @@ -287,15 +287,15 @@ Memory also ships a `values-fips.yaml` overlay. Apply it only to Memory, and con Identity Service release's posture separately: ```bash -helm pull redis-ai/redis-agent-memory --version 0.8.0 --untar +helm pull redis-ai/redis-agent-memory --version 0.10.2 --untar helm upgrade redis-agent-memory redis-ai/redis-agent-memory \ - --version 0.8.0 \ + --version 0.10.2 \ --namespace \ -f ram-values.yaml \ -f redis-agent-memory/values-fips.yaml helm upgrade ids redis-ai/iris-identity-service \ - --version 0.10.0 \ + --version 0.10.2 \ --namespace \ -f values-ids.yaml ``` @@ -346,7 +346,7 @@ the direct caller also has a valid Redis Agent Memory credential. The chart includes `networkpolicy.reference.yaml` at the chart root as a reference manifest. Get it with -`helm pull redis-ai/redis-agent-memory --version 0.8.0 --untar`. It is not templated because allowed callers are +`helm pull redis-ai/redis-agent-memory --version 0.10.2 --untar`. It is not templated because allowed callers are environment-specific. Customize the placeholders before applying it: diff --git a/content/operate/iris/agent-memory/self-managed/prerequisites.md b/content/operate/iris/agent-memory/self-managed/prerequisites.md index 75138b4ff0..1f3323f2a2 100644 --- a/content/operate/iris/agent-memory/self-managed/prerequisites.md +++ b/content/operate/iris/agent-memory/self-managed/prerequisites.md @@ -18,7 +18,7 @@ aliases: Redis Agent Memory is distributed as container images on Docker Hub plus a Helm chart in the Redis AI Helm repository. The chart deploys the Redis Agent Memory Data Plane, Redis Agent Memory workers, and the Redis Agent Memory Control Plane. Install Identity -Service separately with the `iris-identity-service` chart `0.10.0` before connecting Memory chart `0.8.0`. +Service separately with the `iris-identity-service` chart `0.10.2` before connecting Memory chart `0.10.2`. You provide the Redis databases, provider credentials, Kubernetes exposure, and license material used by the deployment. @@ -32,7 +32,7 @@ license material used by the deployment. | Item | Where it comes from | | ---- | ------------------- | | Container images | Docker Hub: `redislabs/agent-memory`, `redislabs/agent-memory-control-plane`, and `redislabs/iris-identity-service` | -| Helm charts | `redis-agent-memory` `0.8.0` and `iris-identity-service` `0.10.0` in the Redis AI Helm repository | +| Helm charts | `redis-agent-memory` `0.10.2` and `iris-identity-service` `0.10.2` in the Redis AI Helm repository | | Redis databases | You provide Store Redis, Job Redis, and Metadata Redis | | License key | Contact your Redis representative or [contact sales](https://redis.io/contact/). | | Provider credentials | You provide embedding provider credentials and, when worker features are enabled, promotion or summarization LLM credentials | @@ -124,14 +124,14 @@ To supply a complete configuration file in your own Secret instead, see ## Release artifacts and image tags -The published Memory chart `0.8.0` defaults `image.tag` and `controlplane.image.tag` to `0.8.0`. -Identity Service chart `0.10.0` defaults its image to its own application version, `0.10.0`. +The published Memory chart `0.10.2` defaults `image.tag` and `controlplane.image.tag` to `0.10.2`. +Identity Service chart `0.10.2` defaults its image to its own application version, `0.10.2`. Set tags explicitly when pinning images or using a mirror. The releases have independent versions. Both charts are published at `https://helm.redis.io/ai` as `redis-ai/redis-agent-memory` and `redis-ai/iris-identity-service`. Their standard images are -`redislabs/agent-memory:0.8.0`, `redislabs/agent-memory-control-plane:0.8.0`, and -`redislabs/iris-identity-service:0.10.0`. +`redislabs/agent-memory:0.10.2`, `redislabs/agent-memory-control-plane:0.10.2`, and +`redislabs/iris-identity-service:0.10.2`. ## Air-gapped and private registry installs @@ -147,9 +147,9 @@ Mirror these images into your internal registry: | `python:3.13-alpine` | Optional, with `tests.smoke` | | `curlimages/curl:8.17.0` | Optional, for `helm test` on the Identity Service release (`tests.image` in `values-ids.yaml`) | -For this release, `` is `0.8.0` and `` is `0.10.0`. -Inspect each package with `helm show chart redis-ai/redis-agent-memory --version 0.8.0` -and `helm show chart redis-ai/iris-identity-service --version 0.10.0`. Memory does not pin +For this release, `` is `0.10.2` and `` is `0.10.2`. +Inspect each package with `helm show chart redis-ai/redis-agent-memory --version 0.10.2` +and `helm show chart redis-ai/iris-identity-service --version 0.10.2`. Memory does not pin or install the Identity Service chart. ```bash @@ -198,7 +198,7 @@ Configure the separate Identity Service release in `values-ids.yaml`: ```yaml image: repository: registry.example.com/redislabs/iris-identity-service - tag: "0.10.0" + tag: "0.10.2" tests: image: registry.example.com/curlimages/curl:8.17.0 global: diff --git a/content/operate/iris/agent-memory/self-managed/reference.md b/content/operate/iris/agent-memory/self-managed/reference.md index 80d5199240..b8706001c2 100644 --- a/content/operate/iris/agent-memory/self-managed/reference.md +++ b/content/operate/iris/agent-memory/self-managed/reference.md @@ -82,8 +82,8 @@ CSI-synced Secrets, make sure a Control Plane pod also mounts the corresponding | Need | Reference | | --- | --- | -| Helm chart values | `values.yaml` at the chart root (`helm pull redis-ai/redis-agent-memory --version 0.8.0 --untar`) | -| Identity Service chart values | `helm show values redis-ai/iris-identity-service --version 0.10.0` | +| Helm chart values | `values.yaml` at the chart root (`helm pull redis-ai/redis-agent-memory --version 0.10.2 --untar`) | +| Identity Service chart values | `helm show values redis-ai/iris-identity-service --version 0.10.2` | | FIPS values overlay | `values-fips.yaml` at the chart root | | NetworkPolicy reference | `networkpolicy.reference.yaml` at the chart root | | Chart README | `README.md` at the chart root | diff --git a/content/operate/iris/agent-memory/view-service.md b/content/operate/iris/agent-memory/view-service.md index 50eb06d6bb..3b8b09d2a7 100644 --- a/content/operate/iris/agent-memory/view-service.md +++ b/content/operate/iris/agent-memory/view-service.md @@ -9,7 +9,6 @@ hideListLinks: true linktitle: View service title: View and manage Agent Memory service weight: 15 -bannerText: Redis Agent Memory on Redis Cloud is currently available as a public preview. Features and behavior are subject to change. aliases: - /operate/rc/context-engine/agent-memory/view-service/ - /operate/rc/agent-memory/view-service/ @@ -51,7 +50,7 @@ The **General settings** section provides the connection details and general set Select the **Copy** button next to the Store ID and API Base URL to copy them to the clipboard. -Follow the [Redis Agent Memory REST API quickstart]({{< relref "/develop/ai/context-engine/agent-memory/rest-api-quickstart" >}}) to use the connection information and API key. +Follow the [Redis Agent Memory quickstart]({{< relref "/develop/ai/context-engine/agent-memory/quickstart" >}}) to use the connection information and API key. ### Memory configuration @@ -101,9 +100,6 @@ To change a custom memory type's name or fields, you must create a new service. The **Sensitive-data exclusions** section shows the [sensitive-data exclusions]({{< relref "/operate/iris/agent-memory/create-service#sensitive-data-exclusions" >}}) configured for the service. -> [!NOTE] -> Sensitive-data exclusions are an early-stage feature, enabled for selected accounts. If you want to try them, contact your Redis representative or [contact sales](https://redis.io/contact/). - ![The Sensitive-data exclusions section for the Agent Memory service.](/images/rc/agent-memory-view-sensitive-data-exclusions.png) | Setting name |Description| diff --git a/layouts/partials/tabbed-clients-example.html b/layouts/partials/tabbed-clients-example.html index 27600358fe..a5bda26b0c 100644 --- a/layouts/partials/tabbed-clients-example.html +++ b/layouts/partials/tabbed-clients-example.html @@ -8,7 +8,7 @@ {{ $cliTabName := (or (.Scratch.Get "cli_tab_name") ">_ Redis CLI") }} {{ $cliFooterLinkText := .Scratch.Get "cli_footer_link_text" }} {{ $cliFooterLinkUrl := .Scratch.Get "cli_footer_link_url" }} -{{ $showFooter := .Scratch.Get "show_footer" | default true }} +{{ $showFooter := .Scratch.Get "show_footer" }} {{/* runnable defaults to true; "false" renders a static, non-executable cli tab */}} {{ $runnable := ne (.Scratch.Get "runnable") "false" }} {{/* try_it defaults to true; "false" hides the external redis.io/cli button but keeps the on-page tab interactive */}} diff --git a/local_examples/agent-memory/agent_memory_sdk.py b/local_examples/agent-memory/agent_memory_sdk.py new file mode 100644 index 0000000000..26130f14b7 --- /dev/null +++ b/local_examples/agent-memory/agent_memory_sdk.py @@ -0,0 +1,129 @@ +# EXAMPLE: agent_memory_sdk + +# STEP_START connect_health_check +import os +from datetime import datetime, timezone + +from redis_agent_memory import AgentMemory, models + + +ENDPOINT = "" +STORE_ID = "" +SESSION_ID = "travel-planning-session" +USER_ID = os.environ.get("OWNER_ID", "quickstart-user") + + +def show(label, response): + print(f"{label}:") + print(response.model_dump_json(by_alias=True, indent=2)) + + +def main(): + with AgentMemory( + ENDPOINT, + store_id=STORE_ID, + api_key=os.environ["API_KEY"], + ) as agent_memory: + health = agent_memory.store_health() + show("Store health", health) + + +if __name__ == "__main__": + main() +# STEP_END + +# STEP_START add_session_event +import os +from datetime import datetime, timezone + +from redis_agent_memory import AgentMemory, models + + +ENDPOINT = "" +STORE_ID = "" +SESSION_ID = "travel-planning-session" +USER_ID = os.environ.get("OWNER_ID", "quickstart-user") + + +def show(label, response): + print(f"{label}:") + print(response.model_dump_json(by_alias=True, indent=2)) + + +def main(): + with AgentMemory( + ENDPOINT, + store_id=STORE_ID, + api_key=os.environ["API_KEY"], + ) as agent_memory: + health = agent_memory.store_health() + show("Store health", health) + + event = agent_memory.add_session_event( + session_id=SESSION_ID, + actor_id=USER_ID, + role=models.MessageRole.USER, + content=[models.Text( + text=( + "I am visiting Tokyo and Kyoto next month. " + "I am vegetarian and prefer spicy food." + ), + )], + created_at=datetime.now(timezone.utc), + ) + show("Created event", event) + + session = agent_memory.get_session_memory( + session_id=SESSION_ID, + ) + show("Session memory", session) + + +if __name__ == "__main__": + main() +# STEP_END + +# STEP_START search_long_term_memory_basic +import os +from datetime import datetime, timezone + +from redis_agent_memory import AgentMemory, models + + +ENDPOINT = "" +STORE_ID = "" +SESSION_ID = "travel-planning-session" +USER_ID = os.environ.get("OWNER_ID", "quickstart-user") + + +def show(label, response): + print(f"{label}:") + print(response.model_dump_json(by_alias=True, indent=2)) + + +def main(): + with AgentMemory( + ENDPOINT, + store_id=STORE_ID, + api_key=os.environ["API_KEY"], + ) as agent_memory: + health = agent_memory.store_health() + show("Store health", health) + + results = agent_memory.search_long_term_memory( + request={ + "text": "What dietary requirements and food preferences does the user have?", + "filter_": { + "owner_id": { + "eq": USER_ID, + } + }, + "limit": 5, + }, + ) + show("Automatically extracted memories", results) + + +if __name__ == "__main__": + main() +# STEP_END diff --git a/local_examples/agent-memory/agent_memory_sdk.ts b/local_examples/agent-memory/agent_memory_sdk.ts new file mode 100644 index 0000000000..fd734a40d0 --- /dev/null +++ b/local_examples/agent-memory/agent_memory_sdk.ts @@ -0,0 +1,122 @@ +// EXAMPLE: agent_memory_sdk + +// STEP_START connect_health_check +import { AgentMemory } from "@redis-iris/agent-memory"; + +const apiKey = process.env.API_KEY; +if (!apiKey) { + throw new Error("Set the API_KEY environment variable."); +} + +const serverURL = ""; +const storeId = ""; +const sessionId = "travel-planning-session"; +const userId = process.env.OWNER_ID ?? "quickstart-user"; + +const agentMemory = new AgentMemory({ + serverURL, + storeId, + apiKey, +}); + +async function run() { + const health = await agentMemory.storeHealth(); + console.log("Store health:"); + console.dir(health, { depth: null }); +} + +run().catch((error) => { + console.error(error instanceof Error ? error.message : "Request failed"); + process.exitCode = 1; +}); +// STEP_END + +// STEP_START add_session_event +import { AgentMemory } from "@redis-iris/agent-memory"; + +const apiKey = process.env.API_KEY; +if (!apiKey) { + throw new Error("Set the API_KEY environment variable."); +} + +const serverURL = ""; +const storeId = ""; +const sessionId = "travel-planning-session"; +const userId = process.env.OWNER_ID ?? "quickstart-user"; + +const agentMemory = new AgentMemory({ + serverURL, + storeId, + apiKey, +}); + +async function run() { + const health = await agentMemory.storeHealth(); + console.log("Store health:"); + console.dir(health, { depth: null }); + + const event = await agentMemory.addSessionEvent({ + sessionId, + actorId: userId, + role: "USER", + content: [{ + text: "I am visiting Tokyo and Kyoto next month. I am vegetarian and prefer spicy food.", + }], + createdAt: new Date(), + }); + console.log("Created event:"); + console.dir(event, { depth: null }); + + const session = await agentMemory.getSessionMemory(sessionId); + console.log("Session memory:"); + console.dir(session, { depth: null }); +} + +run().catch((error) => { + console.error(error instanceof Error ? error.message : "Request failed"); + process.exitCode = 1; +}); +// STEP_END + +// STEP_START search_long_term_memory_basic +import { AgentMemory } from "@redis-iris/agent-memory"; + +const apiKey = process.env.API_KEY; +if (!apiKey) { + throw new Error("Set the API_KEY environment variable."); +} + +const serverURL = ""; +const storeId = ""; +const sessionId = "travel-planning-session"; +const userId = process.env.OWNER_ID ?? "quickstart-user"; + +const agentMemory = new AgentMemory({ + serverURL, + storeId, + apiKey, +}); + +async function run() { + const health = await agentMemory.storeHealth(); + console.log("Store health:"); + console.dir(health, { depth: null }); + + const results = await agentMemory.searchLongTermMemory({ + text: "What dietary requirements and food preferences does the user have?", + filter: { + ownerId: { + eq: userId, + }, + }, + limit: 5, + }); + console.log("Automatically extracted memories:"); + console.dir(results, { depth: null }); +} + +run().catch((error) => { + console.error(error instanceof Error ? error.message : "Request failed"); + process.exitCode = 1; +}); +// STEP_END