Search/Build a Memory Service for AI Agents Without Mixing Memories Between Users

Build a Memory Service for AI Agents Without Mixing Memories Between Users

Use when building or extending a long-term memory service for AI agents. Keeps each memory bank separate, splits storing, searching, and reasoning, and covers database migrations, API client updates, tests judged by a second model, and per-tenant or per-bank settings.

To use this AI rule
## Separate Memory Types and Never Mix Data Between Banks Keep stored knowledge types distinct because ingestion, retrieval, and reasoning use them differently. ### Model memory explicitly - World facts represent general knowledge. - Experience facts represent personal events attributed to a user or agent. - Mental models consolidate patterns synthesized from multiple facts. - Allow each memory bank to carry background context and disposition traits such as skepticism, literalism, and empathy on a 1–5 scale. - Apply dispositions to reflective reasoning only; do not let them alter factual recall. ### Isolate banks - Treat each bank as an independent memory store for one user or agent. - Prevent cross-bank leakage in storage, retrieval, reasoning, and tests. - Operate on exactly 1 bank per API request. - Make clients orchestrate any multi-bank workflow rather than weakening the server boundary. ## Separate Storing, Searching, and Reasoning, and Combine Several Search Methods Separate the three primary operations while sharing stable abstractions for models, embeddings, entities, and ranking. ### Preserve operation semantics - Retain ingests content and extracts facts, entities, and relationships. - Recall retrieves existing memories without applying disposition traits. - Reflect performs disposition-aware reasoning over memories and consolidated mental models. - Keep orchestration separate from fact extraction, entity normalization, link management, and provider adapters. ### Build retrieval as a pipeline - Run 4 complementary retrieval strategies in parallel: semantic, lexical BM25, graph, and temporal retrieval. - Fuse ranked results with reciprocal rank fusion before cross-encoder reranking. - Keep graph expansion behind a retrieval interface so alternative strategies remain interchangeable. - Support local and remote model, embedding, and reranking providers through narrow wrappers. ### Synchronize API contracts - Regenerate the OpenAPI specification after endpoint changes. - Regenerate every supported client SDK, including Python, TypeScript, and Rust, after the specification changes. - When an API parameter changes, update the proxy route, request extraction, SDK call, client type, and any consuming interface together. ## Write One Migration That Works on Both PostgreSQL and Oracle Use one migration history for every supported database while making dialect differences explicit. ### Define the revision - Give each migration a unique 12-character hexadecimal revision identifier and point `down_revision` to its predecessor. - Keep upgrade and downgrade behavior paired. - Dispatch through one dialect router that selects the implementation from the live connection. - Make the migration-shape test fail when the dispatcher is missing. ### Implement each dialect - Provide separate upgrade and downgrade functions for PostgreSQL and Oracle-compatible databases. - Qualify raw PostgreSQL SQL with the configured tenant schema when one is present. - Use driver-level execution only for database forms the migration framework cannot model, such as specialized vector, text, or partition operations. - If a migration genuinely applies to only 1 dialect, omit the other slot deliberately; never leave it empty through oversight. - Consider schema drift across dialects during review. ### Verify execution - Run migrations against the base schema and all tenant schemas. - Also support a targeted run against 1 named tenant schema for focused verification. - When migrations run automatically at service startup, test startup against every supported dialect and tenant scope. ## Pass Required Checks and Code Review Before Pushing Code Use the canonical toolchain after every implementation change and distinguish blocking defects from advisory cleanup. ### Run required checks - Run formatting, linting, and type checking after Python or TypeScript changes. - Treat unused imports and variables, orphaned client files, and unlisted package dependencies as blocking failures. - Treat whole unused functions and intentionally broad component-library exports as advisory unless project policy promotes them. - Run unit tests, API integration tests, and relevant benchmarks through the repository’s standard commands. - Regenerate API specifications and clients when required, then verify that no uncommitted generated diff remains. ### Review before publication - Perform a structured code review after implementation. - Fix every must-fix finding involving missing tests, dead code, type safety, data isolation, or generated-file drift. - Do not push code or create a change request until must-fix findings are resolved. - Do not add unreleased changelog entries manually; let the release process create versioned entries. ## Test Fixed Logic Directly and Have a Second Model Judge Model Answers Choose assertions according to whether the behavior is mechanically deterministic or depends on model interpretation. ### Test deterministic mechanics directly - Use direct assertions for counts, required fields, prompt assembly, suppression logic, and injected substrings. - Keep most tests fast with mock models or pure functions. - Pair model-behavior tests with unit tests that cover the deterministic preparation around them. ### Judge semantic model behavior 1. Mark a semantic test for the single-provider core-model job; reserve the provider-matrix marker for cross-provider acceptance. 2. Call the real configured pipeline rather than a mock. 3. Summarize the response with the semantic labels needed for evaluation. 4. Ask an independent judge model whether the response meets explicit criteria in the supplied context. 5. Never judge with the same model call being tested. - Use judge assertions for classification, attribution, dimension preservation, and instruction following. - Do not hard-code exact strings or enum values for behavior that can validly vary across models and runs. ## Update Every Client When the API Changes, and Add CI for New Integrations An integration is complete only when runtime behavior, tests, continuous integration, release registration, and client surfaces agree. ### Update API consumers - When adding a request field, extract it in the proxy route and pass it to the SDK call. - Update the client method type and every interface that supplies or reads the field. - Keep each request scoped to 1 bank and preserve client-side orchestration for multi-bank use cases. ### Complete an integration - Add tests that mock or exercise the external framework interface and verify that the integration calls the memory service correctly. - Do not treat helper-only unit tests as sufficient integration coverage. - Add a change-scoped continuous-integration job that builds, installs dependencies, and runs the integration tests. - Register the integration in the standard release workflow. - Use typed structures rather than raw dictionaries for structured data, and avoid multi-item tuple returns. - Do not merge or publish while tests, CI change detection, or release registration are missing. ## Decide Whether a New Setting Can Change per Tenant or Bank Before Adding It Classify every setting before implementation so override scope and documentation remain consistent. ### Choose the configuration level - Resolve hierarchical settings from global environment values through tenant overrides to bank-level database overrides. - Use hierarchical configuration for model providers, model names, API keys, operation-specific options, and customer-specific feature flags. - Keep database addresses, hosts, ports, global concurrency limits, and system-wide flags static at server level. - Do not add a static field to the set of remotely configurable fields. ### Add a field completely 1. Define the environment-variable name and default value. 2. Add a typed field to the central configuration object. 3. Add it to the configurable-field allowlist only when tenant or bank overrides are valid. 4. Initialize it from the environment. 5. Add a command-line option only when command-line override is required. 6. Read hierarchical values through the per-bank resolver and static values through the global accessor. 7. Update the configuration reference and environment template with description, default, and hierarchy status. 8. Synchronize every bundled copy of the environment template and keep the template-sync test passing. - Never expose real credential values in documentation, templates, logs, or tests.