From fab34eaa94706f4682fb4a56715f23c6ecaf256e Mon Sep 17 00:00:00 2001 From: Brian Sam-Bodden Date: Wed, 25 Feb 2026 15:38:09 -0700 Subject: [PATCH 1/3] refactor: standardize Redis key and index naming to dash convention (#39) Rename all Redis key prefixes and index names from underscores to dashes to align with RedisVL conventions. Add migration CLI command for existing installations to rename keys in-place without data loss. --- agent_memory_server/cli.py | 76 +++++++++ agent_memory_server/config.py | 8 +- agent_memory_server/memory_vector_db.py | 2 +- agent_memory_server/migrations.py | 156 ++++++++++++++++++ agent_memory_server/utils/keys.py | 6 +- agent_memory_server/working_memory.py | 8 +- agent_memory_server/working_memory_index.py | 2 +- docs/configuration.md | 4 +- ...gent_memory_server_interactive_guide.ipynb | 28 ++-- tests/benchmarks/test_migration_benchmark.py | 4 +- tests/test_long_term_memory.py | 2 +- tests/test_token_auth.py | 4 +- tests/test_working_memory.py | 2 +- 13 files changed, 267 insertions(+), 35 deletions(-) diff --git a/agent_memory_server/cli.py b/agent_memory_server/cli.py index 648de720..5d1237d4 100644 --- a/agent_memory_server/cli.py +++ b/agent_memory_server/cli.py @@ -21,6 +21,7 @@ migrate_add_discrete_memory_extracted_2, migrate_add_memory_hashes_1, migrate_add_memory_type_3, + migrate_redis_key_naming_4, ) from agent_memory_server.utils.redis import get_redis_conn @@ -263,6 +264,81 @@ async def run_migration(): asyncio.run(run_migration()) +@cli.command() +@click.option( + "--batch-size", + default=50, + help="Number of keys to rename per pipeline batch", +) +@click.option( + "--dry-run", + is_flag=True, + help="Show what would change without executing", +) +def migrate_redis_naming(batch_size: int, dry_run: bool): + """ + Migrate Redis key and index names from underscore to dash convention. + + Renames keys: + memory_idx:* → memory-idx:* + working_memory:* → working-memory:* + auth_token:* → auth-token:* + auth_tokens:list → auth-tokens:list + + Drops old indexes and rebuilds with new names. + + Use --dry-run to see what would change without making modifications. + """ + import asyncio + + from agent_memory_server.memory_vector_db import RedisVLMemoryVectorDatabase + from agent_memory_server.memory_vector_db_factory import get_memory_vector_db + from agent_memory_server.working_memory_index import rebuild_working_memory_index + + configure_logging() + + async def run_migration(): + redis = await get_redis_conn() + + if dry_run: + click.echo("Dry run — no changes will be made.\n") + + counts = await migrate_redis_key_naming_4( + redis=redis, batch_size=batch_size, dry_run=dry_run + ) + + click.echo("\nKey rename summary:") + click.echo(f" memory_idx keys: {counts['memory_idx']}") + click.echo(f" working_memory keys: {counts['working_memory']}") + click.echo(f" auth_token keys: {counts['auth_token']}") + click.echo(f" auth_tokens:list: {counts['auth_tokens_list']}") + click.echo(f" indexes dropped: {counts['indexes_dropped']}") + + if dry_run: + click.echo("\nNo changes were made (dry run).") + return + + # Rebuild indexes with new names + click.echo("\nRebuilding indexes with new names...") + + # Rebuild long-term memory index + db = await get_memory_vector_db() + if isinstance(db, RedisVLMemoryVectorDatabase): + index = db.index + await index.create(overwrite=True) + click.echo(f" Rebuilt long-term memory index: {index.name}") + + # Rebuild working memory index + await rebuild_working_memory_index(redis) + click.echo( + f" Rebuilt working memory index: {settings.working_memory_index_name}" + ) + + click.echo("\nMigration completed successfully.") + + asyncio.run(run_migration()) + + @cli.command() @click.option("--port", default=settings.port, help="Port to run the server on") @click.option("--host", default="0.0.0.0", help="Host to run the server on") diff --git a/agent_memory_server/config.py b/agent_memory_server/config.py index edd65f3b..ec5fc661 100644 --- a/agent_memory_server/config.py +++ b/agent_memory_server/config.py @@ -374,7 +374,7 @@ class Settings(BaseSettings): ) # RedisVL configuration (used by default Redis factory) - redisvl_index_name: str = "memory_records" + redisvl_index_name: str = "memory-records" # The server indexes messages in long-term memory by default. If this # setting is enabled, we also extract discrete memories from message text @@ -400,13 +400,13 @@ class Settings(BaseSettings): # TODO: Adapt to memory database settings redisvl_distance_metric: str = "COSINE" redisvl_vector_dimensions: str = "1536" - redisvl_index_prefix: str = "memory_idx" + redisvl_index_prefix: str = "memory-idx" redisvl_indexing_algorithm: str = "HNSW" # Working Memory Index Settings # Used for listing sessions via Redis Search instead of sorted sets - working_memory_index_name: str = "working_memory_idx" - working_memory_index_prefix: str = "working_memory:" + working_memory_index_name: str = "working-memory-idx" + working_memory_index_prefix: str = "working-memory:" # Deduplication Settings (Store-Time) # Distance threshold for semantic similarity when deduplicating at store time diff --git a/agent_memory_server/memory_vector_db.py b/agent_memory_server/memory_vector_db.py index 61eef1f9..7bf7bfb6 100644 --- a/agent_memory_server/memory_vector_db.py +++ b/agent_memory_server/memory_vector_db.py @@ -610,7 +610,7 @@ async def add_memories(self, memories: list[MemoryRecord]) -> list[str]: memory_ids.append(memory.id) # Load into Redis via RedisVL -- use id_field so keys are - # auto-generated with the index prefix (e.g. "memory_idx:"). + # auto-generated with the index prefix (e.g. "memory-idx:"). # Do NOT pass explicit keys, as that bypasses the prefix. await self._index.load(data_list, id_field="id_") return memory_ids diff --git a/agent_memory_server/migrations.py b/agent_memory_server/migrations.py index a1c1495f..7a5c5045 100644 --- a/agent_memory_server/migrations.py +++ b/agent_memory_server/migrations.py @@ -133,3 +133,159 @@ async def migrate_add_memory_type_3(redis: Redis | None = None) -> None: migrated_count += 1 logger.info(f"Migration completed. Added memory_type to {migrated_count} memories") + + +async def migrate_redis_key_naming_4( + redis: Redis | None = None, + batch_size: int = 50, + dry_run: bool = False, +) -> dict[str, int]: + """ + Migration 4: Rename Redis keys and drop old indexes to use dash convention. + + Renames: + - memory_idx:* → memory-idx:* + - working_memory:* → working-memory:* + - auth_token:* → auth-token:* + - auth_tokens:list → auth-tokens:list + + Drops old indexes (without deleting data): + - memory_records + - working_memory_idx + + Args: + redis: Optional Redis client + batch_size: Number of keys to rename per pipeline batch + dry_run: If True, only count keys without renaming + + Returns: + Dict with counts per category + """ + logger.info("Starting Redis key naming migration (underscore → dash)") + redis = redis or await get_redis_conn() + + counts: dict[str, int] = { + "memory_idx": 0, + "working_memory": 0, + "auth_token": 0, + "auth_tokens_list": 0, + "indexes_dropped": 0, + } + + # Migration status keys to skip (they are themselves being renamed) + migration_status_keys = { + b"working_memory:migration:complete", + b"working-memory:migration:complete", + b"working_memory:migration:remaining", + b"working-memory:migration:remaining", + } + + async def _scan_and_rename( + pattern: str, old_prefix: str, new_prefix: str, category: str + ) -> int: + """Scan for keys matching pattern and rename old_prefix to new_prefix.""" + renamed = 0 + cursor = 0 + while True: + cursor, keys = await redis.scan(cursor=cursor, match=pattern, count=1000) + if not keys: + if cursor == 0: + break + continue + + # Filter out migration status keys for working_memory category + if category == "working_memory": + keys = [k for k in keys if k not in migration_status_keys] + + if not keys: + if cursor == 0: + break + continue + + if dry_run: + renamed += len(keys) + else: + # Batch rename using pipeline + for i in range(0, len(keys), batch_size): + batch = keys[i : i + batch_size] + pipe = redis.pipeline() + for key in batch: + key_str = key.decode("utf-8") if isinstance(key, bytes) else key + new_key = key_str.replace(old_prefix, new_prefix, 1) + pipe.rename(key_str, new_key) + await pipe.execute() + renamed += len(batch) + + if cursor == 0: + break + + return renamed + + # 1. Rename memory_idx:* → memory-idx:* + counts["memory_idx"] = await _scan_and_rename( + "memory_idx:*", "memory_idx:", "memory-idx:", "memory_idx" + ) + logger.info( + f"{'Would rename' if dry_run else 'Renamed'} {counts['memory_idx']} memory_idx keys" + ) + + # 2. Rename working_memory:* → working-memory:* + counts["working_memory"] = await _scan_and_rename( + "working_memory:*", "working_memory:", "working-memory:", "working_memory" + ) + logger.info( + f"{'Would rename' if dry_run else 'Renamed'} {counts['working_memory']} working_memory keys" + ) + + # 3. Rename auth_token:* → auth-token:* + counts["auth_token"] = await _scan_and_rename( + "auth_token:*", "auth_token:", "auth-token:", "auth_token" + ) + logger.info( + f"{'Would rename' if dry_run else 'Renamed'} {counts['auth_token']} auth_token keys" + ) + + # 4. Rename auth_tokens:list → auth-tokens:list (single key) + if not dry_run: + exists = await redis.exists("auth_tokens:list") + if exists: + await redis.rename("auth_tokens:list", "auth-tokens:list") + counts["auth_tokens_list"] = 1 + logger.info("Renamed auth_tokens:list → auth-tokens:list") + else: + exists = await redis.exists("auth_tokens:list") + if exists: + counts["auth_tokens_list"] = 1 + logger.info("Would rename auth_tokens:list → auth-tokens:list") + + # 5. Drop old indexes (FT.DROPINDEX without DD flag preserves data) + if not dry_run: + for old_index_name in ("memory_records", "working_memory_idx"): + try: + await redis.execute_command("FT.DROPINDEX", old_index_name) + counts["indexes_dropped"] += 1 + logger.info(f"Dropped old index '{old_index_name}'") + except Exception as e: + # Index may not exist, which is fine + if "Unknown index name" in str(e) or "Unknown Index name" in str(e): + logger.info( + f"Old index '{old_index_name}' does not exist, skipping" + ) + else: + logger.warning(f"Failed to drop index '{old_index_name}': {e}") + else: + for old_index_name in ("memory_records", "working_memory_idx"): + try: + await redis.execute_command("FT.INFO", old_index_name) + counts["indexes_dropped"] += 1 + logger.info(f"Would drop old index '{old_index_name}'") + except Exception: + logger.info(f"Old index '{old_index_name}' does not exist, skipping") + + total = sum(counts.values()) + logger.info( + f"Redis key naming migration {'(dry run) ' if dry_run else ''}complete. " + f"Total: {total} operations ({counts})" + ) + + return counts diff --git a/agent_memory_server/utils/keys.py b/agent_memory_server/utils/keys.py index 72c3cf21..6e06bb2b 100644 --- a/agent_memory_server/utils/keys.py +++ b/agent_memory_server/utils/keys.py @@ -78,7 +78,7 @@ def working_memory_key( ) -> str: """Get the working memory key for a session.""" # Build key components, filtering out None values - key_parts = ["working_memory"] + key_parts = ["working-memory"] if namespace: key_parts.append(namespace) @@ -98,9 +98,9 @@ def search_index_name() -> str: @staticmethod def auth_token_key(token_hash: str) -> str: """Get the auth token key for a hashed token.""" - return f"auth_token:{token_hash}" + return f"auth-token:{token_hash}" @staticmethod def auth_tokens_list_key() -> str: """Get the key for the list of all auth tokens.""" - return "auth_tokens:list" + return "auth-tokens:list" diff --git a/agent_memory_server/working_memory.py b/agent_memory_server/working_memory.py index c4d8a990..ffec7d6d 100644 --- a/agent_memory_server/working_memory.py +++ b/agent_memory_server/working_memory.py @@ -23,8 +23,8 @@ logger = logging.getLogger(__name__) # Redis keys for migration status (shared across workers, persists across restarts) -MIGRATION_STATUS_KEY = "working_memory:migration:complete" -MIGRATION_REMAINING_KEY = "working_memory:migration:remaining" +MIGRATION_STATUS_KEY = "working-memory:migration:complete" +MIGRATION_REMAINING_KEY = "working-memory:migration:remaining" async def check_and_set_migration_status(redis_client: Redis | None = None) -> bool: @@ -62,7 +62,7 @@ async def check_and_set_migration_status(redis_client: Redis | None = None) -> b ) return True - # Scan for working_memory:* keys of type STRING only + # Scan for working-memory:* keys of type STRING only # This is much faster than scanning all keys and calling TYPE on each cursor = 0 string_keys_found = 0 @@ -71,7 +71,7 @@ async def check_and_set_migration_status(redis_client: Redis | None = None) -> b while True: # Use _type="string" to only get string keys directly cursor, keys = await redis_client.scan( - cursor=cursor, match="working_memory:*", count=1000, _type="string" + cursor=cursor, match="working-memory:*", count=1000, _type="string" ) if keys: diff --git a/agent_memory_server/working_memory_index.py b/agent_memory_server/working_memory_index.py index c5e8e01b..393b0323 100644 --- a/agent_memory_server/working_memory_index.py +++ b/agent_memory_server/working_memory_index.py @@ -85,7 +85,7 @@ async def ensure_working_memory_index(redis_client: Redis) -> bool: """ Ensure the working memory search index exists. - Creates a Redis Search index on JSON documents with prefix 'working_memory:' + Creates a Redis Search index on JSON documents with prefix 'working-memory:' if it doesn't already exist. The index enables efficient session listing with filtering by namespace and user_id. diff --git a/docs/configuration.md b/docs/configuration.md index 764cdd97..2337f274 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -101,10 +101,10 @@ INDEX_ALL_MESSAGES_IN_LONG_TERM_MEMORY=false # Index every message (default: fa MEMORY_VECTOR_DB_FACTORY=agent_memory_server.memory_vector_db_factory.create_redis_memory_vector_db # RedisVL Settings (used by default Redis factory) -REDISVL_INDEX_NAME=memory_records # Index name (default: memory_records) +REDISVL_INDEX_NAME=memory-records # Index name (default: memory-records) REDISVL_DISTANCE_METRIC=COSINE # Distance metric (default: COSINE) REDISVL_VECTOR_DIMENSIONS=1536 # Vector dimensions (default: 1536) -REDISVL_INDEX_PREFIX=memory_idx # Index prefix (default: memory_idx) +REDISVL_INDEX_PREFIX=memory-idx # Index prefix (default: memory-idx) REDISVL_INDEXING_ALGORITHM=HNSW # Indexing algorithm (default: HNSW) ``` diff --git a/examples/agent_memory_server_interactive_guide.ipynb b/examples/agent_memory_server_interactive_guide.ipynb index ca0d8b44..8addf3ad 100644 --- a/examples/agent_memory_server_interactive_guide.ipynb +++ b/examples/agent_memory_server_interactive_guide.ipynb @@ -195,7 +195,7 @@ "### Redis Key Structure\n", "\n", "```\n", - "📁 memory_idx/ # Long-term memory index (vector embeddings)\n", + "📁 memory-idx/ # Long-term memory index (vector embeddings)\n", " └── HASH 01KGNPMZQF70S3... # Each memory record (~9KB each)\n", " # Contains: text, embedding vector, metadata\n", " \n", @@ -207,7 +207,7 @@ " └── SORTED SET travel_agent # All sessions in this namespace\n", " # Sorted by last activity time\n", "\n", - "📁 working_memory/ # Session-scoped conversation memory\n", + "📁 working-memory/ # Session-scoped conversation memory\n", " └── 📁 travel_agent/ # Namespace\n", " └── 📁 nitin/ # User ID\n", " └── JSON nitin-travel-session # The actual session data (~944B)\n", @@ -218,15 +218,15 @@ "\n", "| Type | Purpose | Example |\n", "|------|---------|---------|\n", - "| **HASH** | Long-term memory records with vector embeddings | `memory_idx:01KGN...` |\n", - "| **JSON** | Working memory sessions (conversations) | `working_memory:travel_agent:nitin:nitin-travel-session` |\n", + "| **HASH** | Long-term memory records with vector embeddings | `memory-idx:01KGN...` |\n", + "| **JSON** | Working memory sessions (conversations) | `working-memory:travel_agent:nitin:nitin-travel-session` |\n", "| **SORTED SET** | Session index for fast lookup by namespace | `sessions:travel_agent` |\n", "| **STREAM** | Background task queue for async processing | `memory-server:stream` |\n", "\n", "### Data Flow\n", "\n", - "1. **`create_long_term_memory()`** → Creates HASH entries in `memory_idx/`\n", - "2. **`put_working_memory()`** → Creates JSON entry in `working_memory/`\n", + "1. **`create_long_term_memory()`** → Creates HASH entries in `memory-idx/`\n", + "2. **`put_working_memory()`** → Creates JSON entry in `working-memory/`\n", "3. **`memory_prompt()`** → Reads from both and combines them\n", "4. **Background tasks** → Process via `memory-server/stream`\n", "\n", @@ -415,7 +415,7 @@ "\n", "Each session is completely independent. They create separate Redis keys:\n", "```\n", - "📁 working_memory/\n", + "📁 working-memory/\n", " └── 📁 travel_agent/ # Same namespace\n", " └── 📁 nitin/ # Same user\n", " ├── JSON nitin-travel-session # Session 1 (separate conversation)\n", @@ -428,7 +428,7 @@ "\n", "If We Create Two Namespaces (e.g., travel_agent and customer_support)\n", "```\n", - "📁 working_memory/\n", + "📁 working-memory/\n", " ├── 📁 travel_agent/ # Namespace 1\n", " │ └── 📁 nitin/\n", " │ └── JSON nitin-session-1\n", @@ -437,7 +437,7 @@ " └── 📁 nitin/\n", " └── JSON nitin-session-1 # Same session ID, but different namespace!\n", "\n", - "📁 memory_idx/\n", + "📁 memory-idx/\n", " ├── HASH 01KGN... (namespace=travel_agent) # Only searchable within travel_agent\n", " └── HASH 01KGP... (namespace=customer_support) # Only searchable within customer_support\n", "```\n", @@ -449,18 +449,18 @@ "\n", "NOTE:\n", "Working Memory: Scoped by Namespace + User + Session\n", - "```working_memory:{namespace}:{user_id}:{session_id}```\n", + "```working-memory:{namespace}:{user_id}:{session_id}```\n", "\n", "So the same session ID in different namespaces are completely separate:\n", "```\n", - "working_memory:travel_agent:nitin:session-1 # One conversation\n", - "working_memory:customer_support:nitin:session-1 # Completely different conversation\n", + "working-memory:travel_agent:nitin:session-1 # One conversation\n", + "working-memory:customer_support:nitin:session-1 # Completely different conversation\n", "```\n", "\n", "Long-Term Memory: Scoped by Namespace + User\n", "Each memory record has namespace and user_id as metadata fields:\n", "```\n", - "memory_idx:01KGNPMZQF70S3...\n", + "memory-idx:01KGNPMZQF70S3...\n", " ├── text: \"Nitin prefers vegetarian food\"\n", " ├── namespace: \"travel_agent\" ← Scoped to this namespace\n", " ├── user_id: \"nitin\" ← Scoped to this user\n", @@ -1050,7 +1050,7 @@ "\n", " - search_memory: Search long-term memory for relevant information using semantic vector search. Use this when you need to find previously stored information about the user, such as their preferences, past conversations, or important facts. Examples: 'Find information about user food preferences', 'What did they say about their job?', 'Look for travel preferences'. This searches only long-term memory, not current working memory - use get_working_memory for current session info. IMPORTANT: The result includes 'memories' with an 'id' field; use these IDs when calling edit_long_term_memory or delete_long_term_memories.\n", "\n", - " - get_or_create_working_memory: Get the current working memory state including recent messages, temporarily stored memories, and session-specific data. Creates a new session if one doesn't exist. Returns information about whether the session was created or found existing. Use this to check what's already in the current conversation context before deciding whether to search long-term memory or add new information. Examples: Check if user preferences are already loaded in this session, review recent conversation context, see what structured data has been stored for this session.\n", + " - get_or_create_working-memory: Get the current working memory state including recent messages, temporarily stored memories, and session-specific data. Creates a new session if one doesn't exist. Returns information about whether the session was created or found existing. Use this to check what's already in the current conversation context before deciding whether to search long-term memory or add new information. Examples: Check if user preferences are already loaded in this session, review recent conversation context, see what structured data has been stored for this session.\n", "\n", " - lazily_create_long_term_memory: Store new important information as a structured memory that will be promoted to long-term storage. Use this when users share preferences, facts, or important details that should be remembered for future conversations. Examples: 'User is vegetarian', 'Lives in Seattle', 'Works as a software engineer', 'Prefers morning meetings'. The system automatically promotes these memories to long-term storage (lazy creation). For time-bound (episodic) information, include a grounded date phrase in the text (e.g., 'on August 14, 2025') and call get_current_datetime to resolve relative expressions like 'today'/'yesterday'; the backend will set the structured event_date during extraction/promotion. Always check if similar information already exists before creating new memories.\n", "\n", diff --git a/tests/benchmarks/test_migration_benchmark.py b/tests/benchmarks/test_migration_benchmark.py index deb960b0..bed3e052 100644 --- a/tests/benchmarks/test_migration_benchmark.py +++ b/tests/benchmarks/test_migration_benchmark.py @@ -62,7 +62,7 @@ async def cleanup(): deleted = 0 while True: cursor, keys = await async_redis_client.scan( - cursor=cursor, match="working_memory:*", count=1000 + cursor=cursor, match="working-memory:*", count=1000 ) if keys: await async_redis_client.delete(*keys) @@ -308,7 +308,7 @@ async def test_migration_script_performance( cursor = 0 while True: cursor, keys = await async_redis_client.scan( - cursor, match="working_memory:*", count=1000 + cursor, match="working-memory:*", count=1000 ) if keys: pipe = async_redis_client.pipeline() diff --git a/tests/test_long_term_memory.py b/tests/test_long_term_memory.py index a29e0d92..9efdfba8 100644 --- a/tests/test_long_term_memory.py +++ b/tests/test_long_term_memory.py @@ -271,7 +271,7 @@ async def test_extract_memory_structure(self, mock_async_redis_client): args, kwargs = mock_redis.hset.call_args # Check the key format - it includes the memory ID in the key structure - assert "memory_idx:" in args[0] and "test-id" in args[0] + assert "memory-idx:" in args[0] and "test-id" in args[0] # Check the mapping - must use pipe separator to match langchain-redis mapping = kwargs["mapping"] diff --git a/tests/test_token_auth.py b/tests/test_token_auth.py index 12875b36..e7c2ade2 100644 --- a/tests/test_token_auth.py +++ b/tests/test_token_auth.py @@ -338,10 +338,10 @@ def test_auth_token_key(self): token_hash = "test_hash_123" key = Keys.auth_token_key(token_hash) - assert key == f"auth_token:{token_hash}" + assert key == f"auth-token:{token_hash}" def test_auth_tokens_list_key(self): """Test auth tokens list key generation.""" key = Keys.auth_tokens_list_key() - assert key == "auth_tokens:list" + assert key == "auth-tokens:list" diff --git a/tests/test_working_memory.py b/tests/test_working_memory.py index b9458cf3..8f28cdef 100644 --- a/tests/test_working_memory.py +++ b/tests/test_working_memory.py @@ -612,7 +612,7 @@ async def test_migration_status_set_by_set_migration_complete( cursor = 0 while True: cursor, keys = await async_redis_client.scan( - cursor=cursor, match="working_memory:*", count=100 + cursor=cursor, match="working-memory:*", count=100 ) if keys: await async_redis_client.delete(*keys) From 5505f1d6978151d0c4bf6ee8ab87cce0efba55ea Mon Sep 17 00:00:00 2001 From: Brian Sam-Bodden Date: Wed, 25 Feb 2026 15:41:33 -0700 Subject: [PATCH 2/3] fix: use RENAMENX for idempotent key migration Switch from RENAME to RENAMENX so the migration is safe to run multiple times without overwriting already-renamed keys. --- agent_memory_server/migrations.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/agent_memory_server/migrations.py b/agent_memory_server/migrations.py index 7a5c5045..0e733783 100644 --- a/agent_memory_server/migrations.py +++ b/agent_memory_server/migrations.py @@ -205,16 +205,16 @@ async def _scan_and_rename( if dry_run: renamed += len(keys) else: - # Batch rename using pipeline + # Batch rename using pipeline with RENAMENX for idempotency for i in range(0, len(keys), batch_size): batch = keys[i : i + batch_size] pipe = redis.pipeline() for key in batch: key_str = key.decode("utf-8") if isinstance(key, bytes) else key new_key = key_str.replace(old_prefix, new_prefix, 1) - pipe.rename(key_str, new_key) - await pipe.execute() - renamed += len(batch) + pipe.renamenx(key_str, new_key) + results = await pipe.execute() + renamed += sum(1 for r in results if r) if cursor == 0: break @@ -245,13 +245,18 @@ async def _scan_and_rename( f"{'Would rename' if dry_run else 'Renamed'} {counts['auth_token']} auth_token keys" ) - # 4. Rename auth_tokens:list → auth-tokens:list (single key) + # 4. Rename auth_tokens:list → auth-tokens:list (single key, idempotent) if not dry_run: exists = await redis.exists("auth_tokens:list") if exists: - await redis.rename("auth_tokens:list", "auth-tokens:list") - counts["auth_tokens_list"] = 1 - logger.info("Renamed auth_tokens:list → auth-tokens:list") + result = await redis.renamenx("auth_tokens:list", "auth-tokens:list") + if result: + counts["auth_tokens_list"] = 1 + logger.info("Renamed auth_tokens:list → auth-tokens:list") + else: + logger.info( + "auth-tokens:list already exists, skipping auth_tokens:list rename" + ) else: exists = await redis.exists("auth_tokens:list") if exists: From fc298dbdc21f02066cb8b718569f93e990a6818d Mon Sep 17 00:00:00 2001 From: Brian Sam-Bodden Date: Thu, 26 Feb 2026 07:13:05 -0700 Subject: [PATCH 3/3] fix: address PR review comments for key naming migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate batch_size >= 1 in migrate_redis_key_naming_4() and constrain CLI --batch-size with IntRange(min=1) - Fix misleading comment on migration_status_keys (they are skipped, not renamed) - Detect old working_memory:* keys in check_and_set_migration_status() and log a warning to run migrate-redis-naming - Fix incorrect tool name in notebook output (get_or_create_working-memory → get_or_create_working_memory) --- agent_memory_server/cli.py | 1 + agent_memory_server/migrations.py | 6 +- agent_memory_server/working_memory.py | 59 +- ...gent_memory_server_interactive_guide.ipynb | 590 +++++++++--------- 4 files changed, 341 insertions(+), 315 deletions(-) diff --git a/agent_memory_server/cli.py b/agent_memory_server/cli.py index 5d1237d4..2eb0079d 100644 --- a/agent_memory_server/cli.py +++ b/agent_memory_server/cli.py @@ -268,6 +268,7 @@ async def run_migration(): @click.option( "--batch-size", default=50, + type=click.IntRange(min=1), help="Number of keys to rename per pipeline batch", ) @click.option( diff --git a/agent_memory_server/migrations.py b/agent_memory_server/migrations.py index 0e733783..8fbc52f6 100644 --- a/agent_memory_server/migrations.py +++ b/agent_memory_server/migrations.py @@ -172,7 +172,11 @@ async def migrate_redis_key_naming_4( "indexes_dropped": 0, } - # Migration status keys to skip (they are themselves being renamed) + if batch_size < 1: + raise ValueError(f"batch_size must be >= 1, got {batch_size}") + + # Migration status keys to skip (not renamed — the new-prefix keys are + # created fresh by the application after migration) migration_status_keys = { b"working_memory:migration:complete", b"working-memory:migration:complete", diff --git a/agent_memory_server/working_memory.py b/agent_memory_server/working_memory.py index ffec7d6d..ff023f10 100644 --- a/agent_memory_server/working_memory.py +++ b/agent_memory_server/working_memory.py @@ -62,32 +62,53 @@ async def check_and_set_migration_status(redis_client: Redis | None = None) -> b ) return True - # Scan for working-memory:* keys of type STRING only - # This is much faster than scanning all keys and calling TYPE on each + # Scan for working-memory:* AND old working_memory:* keys of type STRING. + # If old-prefix keys still exist the key-naming migration hasn't run yet; + # we treat them the same as new-prefix string keys that need lazy migration. cursor = 0 string_keys_found = 0 + old_prefix_status_keys = { + "working_memory:migration:complete", + "working_memory:migration:remaining", + } try: - while True: - # Use _type="string" to only get string keys directly - cursor, keys = await redis_client.scan( - cursor=cursor, match="working-memory:*", count=1000, _type="string" - ) + for pattern in ("working-memory:*", "working_memory:*"): + cursor = 0 + while True: + cursor, keys = await redis_client.scan( + cursor=cursor, match=pattern, count=1000, _type="string" + ) - if keys: - # Filter out migration status keys (they're also strings) - keys = [ - k - for k in keys - if (k.decode("utf-8") if isinstance(k, bytes) else k) - not in (MIGRATION_STATUS_KEY, MIGRATION_REMAINING_KEY) - ] - string_keys_found += len(keys) - - if cursor == 0: - break + if keys: + # Filter out migration status keys (they're also strings) + keys = [ + k + for k in keys + if (k.decode("utf-8") if isinstance(k, bytes) else k) + not in ( + MIGRATION_STATUS_KEY, + MIGRATION_REMAINING_KEY, + *old_prefix_status_keys, + ) + ] + string_keys_found += len(keys) + + if cursor == 0: + break if string_keys_found > 0: + # Check if any old-prefix keys exist and log a hint + old_cursor = 0 + old_cursor, old_keys = await redis_client.scan( + cursor=old_cursor, match="working_memory:*", count=1 + ) + if old_keys: + logger.warning( + "Found working_memory:* keys with old underscore prefix. " + "Run 'agent-memory migrate-redis-naming' to rename them." + ) + # Store the count in Redis for atomic decrement during lazy migration await redis_client.set(MIGRATION_REMAINING_KEY, str(string_keys_found)) logger.info( diff --git a/examples/agent_memory_server_interactive_guide.ipynb b/examples/agent_memory_server_interactive_guide.ipynb index 8addf3ad..93ed5aca 100644 --- a/examples/agent_memory_server_interactive_guide.ipynb +++ b/examples/agent_memory_server_interactive_guide.ipynb @@ -56,8 +56,8 @@ "\n", "### Two-Tier Architecture\n", "```\n", - "Working Memory (Session-scoped) → Long-term Memory (Persistent)\n", - " ↓ ↓\n", + "Working Memory (Session-scoped) \u2192 Long-term Memory (Persistent)\n", + " \u2193 \u2193\n", "- Messages - Semantic search\n", "- Structured memories - Topic modeling \n", "- Summary of past messages - Entity recognition\n", @@ -92,7 +92,7 @@ "text": [ "Base URL: http://localhost:8000\n", "Namespace: travel_agent\n", - "OpenAI API Key: ✓ loaded\n" + "OpenAI API Key: \u2713 loaded\n" ] } ], @@ -115,7 +115,7 @@ "\n", "print(f\"Base URL: {BASE_URL}\")\n", "print(f\"Namespace: {NAMESPACE}\")\n", - "print(f\"OpenAI API Key: {'✓ loaded' if os.environ.get('OPENAI_API_KEY') else '✗ not found'}\")" + "print(f\"OpenAI API Key: {'\u2713 loaded' if os.environ.get('OPENAI_API_KEY') else '\u2717 not found'}\")" ] }, { @@ -188,29 +188,29 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 🔍 What's Happening Behind the Scenes in Redis?\n", + "## \ud83d\udd0d What's Happening Behind the Scenes in Redis?\n", "\n", "As you run this notebook, data is being stored in Redis. Here's what the data structure looks like (as seen in Redis Insight):\n", "\n", "### Redis Key Structure\n", "\n", "```\n", - "📁 memory-idx/ # Long-term memory index (vector embeddings)\n", - " └── HASH 01KGNPMZQF70S3... # Each memory record (~9KB each)\n", + "\ud83d\udcc1 memory-idx/ # Long-term memory index (vector embeddings)\n", + " \u2514\u2500\u2500 HASH 01KGNPMZQF70S3... # Each memory record (~9KB each)\n", " # Contains: text, embedding vector, metadata\n", " \n", - "📁 memory-server/ # Background task queue (Docket)\n", - " └── 📁 runs/ # Task execution history\n", - " └── STREAM stream # Task stream for workers\n", + "\ud83d\udcc1 memory-server/ # Background task queue (Docket)\n", + " \u2514\u2500\u2500 \ud83d\udcc1 runs/ # Task execution history\n", + " \u2514\u2500\u2500 STREAM stream # Task stream for workers\n", "\n", - "📁 sessions/ # Session tracking\n", - " └── SORTED SET travel_agent # All sessions in this namespace\n", + "\ud83d\udcc1 sessions/ # Session tracking\n", + " \u2514\u2500\u2500 SORTED SET travel_agent # All sessions in this namespace\n", " # Sorted by last activity time\n", "\n", - "📁 working-memory/ # Session-scoped conversation memory\n", - " └── 📁 travel_agent/ # Namespace\n", - " └── 📁 nitin/ # User ID\n", - " └── JSON nitin-travel-session # The actual session data (~944B)\n", + "\ud83d\udcc1 working-memory/ # Session-scoped conversation memory\n", + " \u2514\u2500\u2500 \ud83d\udcc1 travel_agent/ # Namespace\n", + " \u2514\u2500\u2500 \ud83d\udcc1 nitin/ # User ID\n", + " \u2514\u2500\u2500 JSON nitin-travel-session # The actual session data (~944B)\n", " # Contains: messages, context, metadata\n", "```\n", "\n", @@ -225,12 +225,12 @@ "\n", "### Data Flow\n", "\n", - "1. **`create_long_term_memory()`** → Creates HASH entries in `memory-idx/`\n", - "2. **`put_working_memory()`** → Creates JSON entry in `working-memory/`\n", - "3. **`memory_prompt()`** → Reads from both and combines them\n", - "4. **Background tasks** → Process via `memory-server/stream`\n", + "1. **`create_long_term_memory()`** \u2192 Creates HASH entries in `memory-idx/`\n", + "2. **`put_working_memory()`** \u2192 Creates JSON entry in `working-memory/`\n", + "3. **`memory_prompt()`** \u2192 Reads from both and combines them\n", + "4. **Background tasks** \u2192 Process via `memory-server/stream`\n", "\n", - "> 💡 **Tip**: Open Redis Insight at `http://localhost:16381` to explore the data in real-time as you run cells!" + "> \ud83d\udca1 **Tip**: Open Redis Insight at `http://localhost:16381` to explore the data in real-time as you run cells!" ] }, { @@ -247,24 +247,24 @@ "\n", "| Pattern | Who Decides | Best For | Memory Flow |\n", "|---------|-------------|----------|-------------|\n", - "| **Code-Driven (SDK)** | Your code | Apps, workflows, deterministic behavior | `Code → SDK → Memory` |\n", - "| **LLM-Driven (Tools)** | The LLM | Conversational agents, chatbots | `LLM ↔ Tools ↔ Memory` |\n", - "| **Background Extraction** | Automatic | Learning systems, passive accumulation | `Conversation → Auto Extract → Memory` |\n", + "| **Code-Driven (SDK)** | Your code | Apps, workflows, deterministic behavior | `Code \u2192 SDK \u2192 Memory` |\n", + "| **LLM-Driven (Tools)** | The LLM | Conversational agents, chatbots | `LLM \u2194 Tools \u2194 Memory` |\n", + "| **Background Extraction** | Automatic | Learning systems, passive accumulation | `Conversation \u2192 Auto Extract \u2192 Memory` |\n", "\n", "### Decision Guide\n", "\n", "```\n", "Start here: Do you need predictable, deterministic memory behavior?\n", - " │\n", - " ├── YES → Use Pattern 1: Code-Driven (SDK)\n", - " │ Your code explicitly stores/retrieves memories\n", - " │\n", - " └── NO → Do you want the LLM to decide what to remember?\n", - " │\n", - " ├── YES → Use Pattern 2: LLM-Driven (Tools)\n", - " │ LLM uses memory tools during conversation\n", - " │\n", - " └── NO → Use Pattern 3: Background Extraction\n", + " \u2502\n", + " \u251c\u2500\u2500 YES \u2192 Use Pattern 1: Code-Driven (SDK)\n", + " \u2502 Your code explicitly stores/retrieves memories\n", + " \u2502\n", + " \u2514\u2500\u2500 NO \u2192 Do you want the LLM to decide what to remember?\n", + " \u2502\n", + " \u251c\u2500\u2500 YES \u2192 Use Pattern 2: LLM-Driven (Tools)\n", + " \u2502 LLM uses memory tools during conversation\n", + " \u2502\n", + " \u2514\u2500\u2500 NO \u2192 Use Pattern 3: Background Extraction\n", " System automatically learns from conversations\n", "```\n", "\n", @@ -287,23 +287,23 @@ "### How It Works\n", "\n", "```\n", - "┌─────────────────────────────────────────────────────────────────┐\n", - "│ YOUR APPLICATION CODE │\n", - "├─────────────────────────────────────────────────────────────────┤\n", - "│ │\n", - "│ 1. User sends message │\n", - "│ ↓ │\n", - "│ 2. YOUR CODE calls memory_prompt() to get context │\n", - "│ ↓ │\n", - "│ 3. Send enriched prompt to LLM │\n", - "│ ↓ │\n", - "│ 4. LLM generates response │\n", - "│ ↓ │\n", - "│ 5. YOUR CODE decides what to store │\n", - "│ ↓ │\n", - "│ 6. Call create_long_term_memory() or put_working_memory() │\n", - "│ │\n", - "└─────────────────────────────────────────────────────────────────┘\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 YOUR APPLICATION CODE \u2502\n", + "\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n", + "\u2502 \u2502\n", + "\u2502 1. User sends message \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 2. YOUR CODE calls memory_prompt() to get context \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 3. Send enriched prompt to LLM \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 4. LLM generates response \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 5. YOUR CODE decides what to store \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 6. Call create_long_term_memory() or put_working_memory() \u2502\n", + "\u2502 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", "```\n", "\n", "**Key Point**: YOUR CODE makes all memory decisions, not the LLM.\n", @@ -415,11 +415,11 @@ "\n", "Each session is completely independent. They create separate Redis keys:\n", "```\n", - "📁 working-memory/\n", - " └── 📁 travel_agent/ # Same namespace\n", - " └── 📁 nitin/ # Same user\n", - " ├── JSON nitin-travel-session # Session 1 (separate conversation)\n", - " └── JSON nitin-travel-session-2 # Session 2 (separate conversation)\n", + "\ud83d\udcc1 working-memory/\n", + " \u2514\u2500\u2500 \ud83d\udcc1 travel_agent/ # Same namespace\n", + " \u2514\u2500\u2500 \ud83d\udcc1 nitin/ # Same user\n", + " \u251c\u2500\u2500 JSON nitin-travel-session # Session 1 (separate conversation)\n", + " \u2514\u2500\u2500 JSON nitin-travel-session-2 # Session 2 (separate conversation)\n", "```\n", "- Each session has its own conversation history (messages)\n", "- Each session has its own context window (summarization happens independently)\n", @@ -428,18 +428,18 @@ "\n", "If We Create Two Namespaces (e.g., travel_agent and customer_support)\n", "```\n", - "📁 working-memory/\n", - " ├── 📁 travel_agent/ # Namespace 1\n", - " │ └── 📁 nitin/\n", - " │ └── JSON nitin-session-1\n", - " │\n", - " └── 📁 customer_support/ # Namespace 2 (completely separate)\n", - " └── 📁 nitin/\n", - " └── JSON nitin-session-1 # Same session ID, but different namespace!\n", - "\n", - "📁 memory-idx/\n", - " ├── HASH 01KGN... (namespace=travel_agent) # Only searchable within travel_agent\n", - " └── HASH 01KGP... (namespace=customer_support) # Only searchable within customer_support\n", + "\ud83d\udcc1 working-memory/\n", + " \u251c\u2500\u2500 \ud83d\udcc1 travel_agent/ # Namespace 1\n", + " \u2502 \u2514\u2500\u2500 \ud83d\udcc1 nitin/\n", + " \u2502 \u2514\u2500\u2500 JSON nitin-session-1\n", + " \u2502\n", + " \u2514\u2500\u2500 \ud83d\udcc1 customer_support/ # Namespace 2 (completely separate)\n", + " \u2514\u2500\u2500 \ud83d\udcc1 nitin/\n", + " \u2514\u2500\u2500 JSON nitin-session-1 # Same session ID, but different namespace!\n", + "\n", + "\ud83d\udcc1 memory-idx/\n", + " \u251c\u2500\u2500 HASH 01KGN... (namespace=travel_agent) # Only searchable within travel_agent\n", + " \u2514\u2500\u2500 HASH 01KGP... (namespace=customer_support) # Only searchable within customer_support\n", "```\n", "\n", "- Same user can have different memories per namespace\n", @@ -461,10 +461,10 @@ "Each memory record has namespace and user_id as metadata fields:\n", "```\n", "memory-idx:01KGNPMZQF70S3...\n", - " ├── text: \"Nitin prefers vegetarian food\"\n", - " ├── namespace: \"travel_agent\" ← Scoped to this namespace\n", - " ├── user_id: \"nitin\" ← Scoped to this user\n", - " └── vector: [...]\n", + " \u251c\u2500\u2500 text: \"Nitin prefers vegetarian food\"\n", + " \u251c\u2500\u2500 namespace: \"travel_agent\" \u2190 Scoped to this namespace\n", + " \u251c\u2500\u2500 user_id: \"nitin\" \u2190 Scoped to this user\n", + " \u2514\u2500\u2500 vector: [...]\n", "```\n", "\n", "When you search, you filter by namespace:\n", @@ -477,31 +477,31 @@ "```\n", "\n", "```\n", - "┌─────────────────────────────────────────────────────────────────┐\n", - "│ NAMESPACE: travel_agent │\n", - "├─────────────────────────────────────────────────────────────────┤\n", - "│ WORKING MEMORY │\n", - "│ ├── nitin:nitin-travel-session (Japan trip conversation) │\n", - "│ └── nitin:nitin-travel-session-2 (Italy trip conversation) │\n", - "│ │\n", - "│ LONG-TERM MEMORY │\n", - "│ ├── \"Nitin prefers vegetarian food\" │\n", - "│ ├── \"Nitin likes hiking\" │\n", - "│ └── \"Nitin prefers mid-tier hotels\" │\n", - "└─────────────────────────────────────────────────────────────────┘\n", - "\n", - "┌─────────────────────────────────────────────────────────────────┐\n", - "│ NAMESPACE: customer_support │\n", - "├─────────────────────────────────────────────────────────────────┤\n", - "│ WORKING MEMORY │\n", - "│ └── nitin:support-ticket-123 (Support conversation) │\n", - "│ │\n", - "│ LONG-TERM MEMORY │\n", - "│ ├── \"Nitin had billing issue in January\" │\n", - "│ └── \"Nitin prefers email over phone\" │\n", - "│ │\n", - "│ ⚠️ CANNOT see travel_agent memories! │\n", - "└─────────────────────────────────────────────────────────────────┘\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 NAMESPACE: travel_agent \u2502\n", + "\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n", + "\u2502 WORKING MEMORY \u2502\n", + "\u2502 \u251c\u2500\u2500 nitin:nitin-travel-session (Japan trip conversation) \u2502\n", + "\u2502 \u2514\u2500\u2500 nitin:nitin-travel-session-2 (Italy trip conversation) \u2502\n", + "\u2502 \u2502\n", + "\u2502 LONG-TERM MEMORY \u2502\n", + "\u2502 \u251c\u2500\u2500 \"Nitin prefers vegetarian food\" \u2502\n", + "\u2502 \u251c\u2500\u2500 \"Nitin likes hiking\" \u2502\n", + "\u2502 \u2514\u2500\u2500 \"Nitin prefers mid-tier hotels\" \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", + "\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 NAMESPACE: customer_support \u2502\n", + "\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n", + "\u2502 WORKING MEMORY \u2502\n", + "\u2502 \u2514\u2500\u2500 nitin:support-ticket-123 (Support conversation) \u2502\n", + "\u2502 \u2502\n", + "\u2502 LONG-TERM MEMORY \u2502\n", + "\u2502 \u251c\u2500\u2500 \"Nitin had billing issue in January\" \u2502\n", + "\u2502 \u2514\u2500\u2500 \"Nitin prefers email over phone\" \u2502\n", + "\u2502 \u2502\n", + "\u2502 \u26a0\ufe0f CANNOT see travel_agent memories! \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", "```\n" ] }, @@ -1004,23 +1004,23 @@ "### How It Works\n", "\n", "```\n", - "┌─────────────────────────────────────────────────────────────────┐\n", - "│ LLM DECIDES │\n", - "├─────────────────────────────────────────────────────────────────┤\n", - "│ │\n", - "│ 1. User sends message │\n", - "│ ↓ │\n", - "│ 2. Send to LLM WITH memory tool schemas │\n", - "│ ↓ │\n", - "│ 3. LLM DECIDES: \"Should I store/search memory?\" │\n", - "│ ↓ │\n", - "│ 4. If yes → LLM returns tool_calls │\n", - "│ ↓ │\n", - "│ 5. YOUR CODE executes tool calls via resolve_function_call() │\n", - "│ ↓ │\n", - "│ 6. Return results to LLM for final response │\n", - "│ │\n", - "└─────────────────────────────────────────────────────────────────┘\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 LLM DECIDES \u2502\n", + "\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n", + "\u2502 \u2502\n", + "\u2502 1. User sends message \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 2. Send to LLM WITH memory tool schemas \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 3. LLM DECIDES: \"Should I store/search memory?\" \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 4. If yes \u2192 LLM returns tool_calls \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 5. YOUR CODE executes tool calls via resolve_function_call() \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 6. Return results to LLM for final response \u2502\n", + "\u2502 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", "```\n", "\n", "**Key Point**: The LLM decides when to use memory, your code just executes.\n", @@ -1050,7 +1050,7 @@ "\n", " - search_memory: Search long-term memory for relevant information using semantic vector search. Use this when you need to find previously stored information about the user, such as their preferences, past conversations, or important facts. Examples: 'Find information about user food preferences', 'What did they say about their job?', 'Look for travel preferences'. This searches only long-term memory, not current working memory - use get_working_memory for current session info. IMPORTANT: The result includes 'memories' with an 'id' field; use these IDs when calling edit_long_term_memory or delete_long_term_memories.\n", "\n", - " - get_or_create_working-memory: Get the current working memory state including recent messages, temporarily stored memories, and session-specific data. Creates a new session if one doesn't exist. Returns information about whether the session was created or found existing. Use this to check what's already in the current conversation context before deciding whether to search long-term memory or add new information. Examples: Check if user preferences are already loaded in this session, review recent conversation context, see what structured data has been stored for this session.\n", + " - get_or_create_working_memory: Get the current working memory state including recent messages, temporarily stored memories, and session-specific data. Creates a new session if one doesn't exist. Returns information about whether the session was created or found existing. Use this to check what's already in the current conversation context before deciding whether to search long-term memory or add new information. Examples: Check if user preferences are already loaded in this session, review recent conversation context, see what structured data has been stored for this session.\n", "\n", " - lazily_create_long_term_memory: Store new important information as a structured memory that will be promoted to long-term storage. Use this when users share preferences, facts, or important details that should be remembered for future conversations. Examples: 'User is vegetarian', 'Lives in Seattle', 'Works as a software engineer', 'Prefers morning meetings'. The system automatically promotes these memories to long-term storage (lazy creation). For time-bound (episodic) information, include a grounded date phrase in the text (e.g., 'on August 14, 2025') and call get_current_datetime to resolve relative expressions like 'today'/'yesterday'; the backend will set the structured event_date during extraction/promotion. Always check if similar information already exists before creating new memories.\n", "\n", @@ -1107,7 +1107,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "✓ OpenAI client initialized\n", + "\u2713 OpenAI client initialized\n", "User: Hi! I'm Nitin. I'm planning a trip to Tokyo and want to find good vegetarian restaurants.\n", "\n", "Sending to LLM with memory tools...\n" @@ -1124,12 +1124,12 @@ "# Check if OpenAI API key is available\n", "OPENAI_API_KEY = os.environ.get(\"OPENAI_API_KEY\")\n", "if not OPENAI_API_KEY:\n", - " print(\"⚠️ OPENAI_API_KEY not set - LLM-driven pattern cells will be skipped\")\n", + " print(\"\u26a0\ufe0f OPENAI_API_KEY not set - LLM-driven pattern cells will be skipped\")\n", " print(\" Set it with: export OPENAI_API_KEY=your-key-here\")\n", " openai_client = None\n", "else:\n", " openai_client = openai.AsyncOpenAI()\n", - " print(\"✓ OpenAI client initialized\")\n", + " print(\"\u2713 OpenAI client initialized\")\n", "\n", "SESSION_ID_LLM = \"nitin-llm-session\"\n", "USER_ID_LLM = \"nitin\"\n", @@ -1419,22 +1419,22 @@ "### How It Works\n", "\n", "```\n", - "┌─────────────────────────────────────────────────────────────────┐\n", - "│ AUTOMATIC EXTRACTION │\n", - "├─────────────────────────────────────────────────────────────────┤\n", - "│ │\n", - "│ 1. Conversation happens normally │\n", - "│ ↓ │\n", - "│ 2. Store conversation in working memory │\n", - "│ ↓ │\n", - "│ 3. SYSTEM AUTOMATICALLY: │\n", - "│ - Analyzes conversation for important info │\n", - "│ - Extracts structured memories │\n", - "│ - Applies contextual grounding │\n", - "│ - Deduplicates similar memories │\n", - "│ - Stores in long-term memory │\n", - "│ │\n", - "└─────────────────────────────────────────────────────────────────┘\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 AUTOMATIC EXTRACTION \u2502\n", + "\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n", + "\u2502 \u2502\n", + "\u2502 1. Conversation happens normally \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 2. Store conversation in working memory \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 3. SYSTEM AUTOMATICALLY: \u2502\n", + "\u2502 - Analyzes conversation for important info \u2502\n", + "\u2502 - Extracts structured memories \u2502\n", + "\u2502 - Applies contextual grounding \u2502\n", + "\u2502 - Deduplicates similar memories \u2502\n", + "\u2502 - Stores in long-term memory \u2502\n", + "\u2502 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", "```\n", "\n", "**Key Point**: Just store conversations, the system learns automatically.\n", @@ -1727,10 +1727,10 @@ "- Can be configured as an environment variable\n", "\n", "```\n", - "Message 1 arrives → Schedule extraction for T+30s\n", - "Message 2 arrives (T+10s) → Reschedule extraction for T+40s\n", - "Message 3 arrives (T+25s) → Reschedule extraction for T+55s\n", - "No more messages → Extraction runs at T+55s\n", + "Message 1 arrives \u2192 Schedule extraction for T+30s\n", + "Message 2 arrives (T+10s) \u2192 Reschedule extraction for T+40s\n", + "Message 3 arrives (T+25s) \u2192 Reschedule extraction for T+55s\n", + "No more messages \u2192 Extraction runs at T+55s\n", "```\n", "\n", "This ensures extraction happens after a \"quiet period\" in the conversation, not after every single message. Although sometimes we prefer that.\n", @@ -1742,32 +1742,32 @@ "Example of too short:\n", "```\n", "T+0s: User: \"I'm planning a trip to Japan next month.\"\n", - "T+5s: ⚡ EXTRACTION RUNS (debounce expired)\n", - " → Extracts: \"User planning trip to Japan\"\n", + "T+5s: \u26a1 EXTRACTION RUNS (debounce expired)\n", + " \u2192 Extracts: \"User planning trip to Japan\"\n", " \n", "T+8s: User: \"I want to visit Tokyo and Kyoto.\"\n", - "T+13s: ⚡ EXTRACTION RUNS\n", - " → Extracts: \"User wants to visit Tokyo and Kyoto\"\n", + "T+13s: \u26a1 EXTRACTION RUNS\n", + " \u2192 Extracts: \"User wants to visit Tokyo and Kyoto\"\n", " \n", "T+16s: User: \"But I'm vegetarian and need wheelchair accessible hotels.\"\n", - "T+21s: ⚡ EXTRACTION RUNS\n", - " → Extracts: \"User is vegetarian, needs accessible hotels\"\n", + "T+21s: \u26a1 EXTRACTION RUNS\n", + " \u2192 Extracts: \"User is vegetarian, needs accessible hotels\"\n", "```\n", "\n", "Alternatively at 30 secs:\n", "\n", "```\n", "T+0s: User: \"I'm planning a trip to Japan next month.\"\n", - " → Schedule extraction for T+30s\n", + " \u2192 Schedule extraction for T+30s\n", " \n", "T+8s: User: \"I want to visit Tokyo and Kyoto.\"\n", - " → Reschedule extraction for T+38s\n", + " \u2192 Reschedule extraction for T+38s\n", " \n", "T+16s: User: \"But I'm vegetarian and hotels with a pool.\"\n", - " → Reschedule extraction for T+46s\n", + " \u2192 Reschedule extraction for T+46s\n", " \n", - "T+46s: ⚡ EXTRACTION RUNS (30s of quiet)\n", - " → Extracts: \"User planning Japan trip to Tokyo and Kyoto, \n", + "T+46s: \u26a1 EXTRACTION RUNS (30s of quiet)\n", + " \u2192 Extracts: \"User planning Japan trip to Tokyo and Kyoto, \n", " is vegetarian, needs hotels with a pool.\"\n", "```" ] @@ -1804,140 +1804,140 @@ "Here's how the APIs work together in a typical conversation:\n", "\n", "```\n", - "┌─────────────────────────────────────────────────────────────────────────────────────────┐\n", - "│ AGENT MEMORY API FLOW EXAMPLE │\n", - "│ User: \"I'm planning a trip to Japan\" │\n", - "└─────────────────────────────────────────────────────────────────────────────────────────┘\n", - "\n", - " ┌──────────────┐\n", - " │ USER │\n", - " └──────┬───────┘\n", - " │\n", - " ▼\n", - "┌──────────────────────────────────────────────────────────────────────────────────────────┐\n", - "│ STEP 1: Get/Create Session │\n", - "│ ─────────────────────────────────────────────────────────────────────────────────────── │\n", - "│ │\n", - "│ client.get_or_create_working_memory(session_id=\"nitin-travel-session\") │\n", - "│ │ │\n", - "│ ▼ │\n", - "│ ┌────────────────────────┐ │\n", - "│ │ WORKING MEMORY │ ← Session-scoped conversation state │\n", - "│ │ ┌──────────────────┐ │ │\n", - "│ │ │ messages: [] │ │ │\n", - "│ │ │ memories: [] │ │ │\n", - "│ │ │ context: null │ │ │\n", - "│ │ └──────────────────┘ │ │\n", - "│ └────────────────────────┘ │\n", - "└──────────────────────────────────────────────────────────────────────────────────────────┘\n", - " │\n", - " ▼\n", - "┌──────────────────────────────────────────────────────────────────────────────────────────┐\n", - "│ STEP 2: Hydrate Context with Memories │\n", - "│ ─────────────────────────────────────────────────────────────────────────────────────── │\n", - "│ │\n", - "│ client.memory_prompt(query=\"I'm planning a trip to Japan\", session_id=...) │\n", - "│ │ │\n", - "│ ├──────────────────────────────────────┐ │\n", - "│ ▼ ▼ │\n", - "│ ┌────────────────────────┐ ┌────────────────────────────────┐ │\n", - "│ │ WORKING MEMORY │ │ LONG-TERM MEMORY │ │\n", - "│ │ (conversation history)│ │ (semantic vector search) │ │\n", - "│ └───────────┬────────────┘ └───────────────┬────────────────┘ │\n", - "│ │ │ │\n", - "│ │ ┌─────────────────────────────────┘ │\n", - "│ │ │ Finds: \"User prefers vegetarian food\" │\n", - "│ │ │ \"User likes hiking\" │\n", - "│ ▼ ▼ │\n", - "│ ┌─────────────────────────────────────────────────────┐ │\n", - "│ │ HYDRATED MESSAGES │ │\n", - "│ │ ┌─────────────────────────────────────────────────┐│ │\n", - "│ │ │ [SYSTEM] Long term memories: ││ │\n", - "│ │ │ - User prefers vegetarian food ││ │\n", - "│ │ │ - User likes hiking ││ │\n", - "│ │ ├─────────────────────────────────────────────────┤│ │\n", - "│ │ │ [USER] I'm planning a trip to Japan ││ │\n", - "│ │ └─────────────────────────────────────────────────┘│ │\n", - "│ └─────────────────────────────────────────────────────┘ │\n", - "└──────────────────────────────────────────────────────────────────────────────────────────┘\n", - " │\n", - " ▼\n", - "┌──────────────────────────────────────────────────────────────────────────────────────────┐\n", - "│ STEP 3: Send to LLM (with optional tool schemas) │\n", - "│ ─────────────────────────────────────────────────────────────────────────────────────── │\n", - "│ │\n", - "│ tools = client.get_all_memory_tool_schemas() ← Optional: Let LLM use memory tools │\n", - "│ │\n", - "│ openai.chat.completions.create( │\n", - "│ messages=hydrated_messages, │\n", - "│ tools=tools.to_list() ← search_memory, add_memory, etc. │\n", - "│ ) │\n", - "│ │ │\n", - "│ ▼ │\n", - "│ ┌─────────────────────────────────────────────────────┐ │\n", - "│ │ LLM RESPONSE │ │\n", - "│ │ ┌─────────────────────────────────────────────────┐│ │\n", - "│ │ │ Option A: Direct response ││ │\n", - "│ │ │ \"Based on your preferences, I recommend...\" ││ │\n", - "│ │ ├─────────────────────────────────────────────────┤│ │\n", - "│ │ │ Option B: Tool call ││ │\n", - "│ │ │ tool_calls: [{name: \"add_memory\", args: ...}] ││ │\n", - "│ │ └─────────────────────────────────────────────────┘│ │\n", - "│ └─────────────────────────────────────────────────────┘ │\n", - "└──────────────────────────────────────────────────────────────────────────────────────────┘\n", - " │\n", - " ┌────────────────┴────────────────┐\n", - " ▼ ▼\n", - "┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────────┐\n", - "│ STEP 4a: If LLM made tool calls │ │ STEP 4b: Store conversation │\n", - "│ ───────────────────────────────────── │ │ ───────────────────────────────────────── │\n", - "│ │ │ │\n", - "│ client.resolve_function_call( │ │ client.put_working_memory( │\n", - "│ function_name=\"add_memory\", │ │ session_id, │\n", - "│ function_arguments={...}, │ │ WorkingMemory(messages=[ │\n", - "│ session_id=... │ │ {role: \"user\", content: \"...\"}, │\n", - "│ ) │ │ {role: \"assistant\", content: \"...\"}│\n", - "│ │ │ │ ]) │\n", - "│ ▼ │ │ ) │\n", - "│ ┌─────────────────────────┐ │ │ │ │\n", - "│ │ ToolCallResolutionResult│ │ │ ▼ │\n", - "│ │ ┌─────────────────────┐ │ │ │ ┌─────────────────────────────────────┐ │\n", - "│ │ │ success: true │ │ │ │ │ Triggers background extraction │ │\n", - "│ │ │ result: {...} │ │ │ │ │ (after EXTRACTION_DEBOUNCE_SECONDS)│ │\n", - "│ │ │ formatted_response: │ │ │ │ │ │ │ │\n", - "│ │ │ \"Memory stored\" │ │ │ │ │ ▼ │ │\n", - "│ │ └─────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │\n", - "│ └─────────────────────────┘ │ │ │ │ Extracted memories: │ │ │\n", - "│ │ │ │ │ - \"User planning Japan trip\"│ │ │\n", - "│ Send formatted_response back to LLM │ │ │ │ - \"User interested in Tokyo\"│ │ │\n", - "│ for final response │ │ │ └─────────────────────────────┘ │ │\n", - "│ │ │ └─────────────────────────────────────┘ │\n", - "└─────────────────────────────────────────┘ └─────────────────────────────────────────────┘\n", - "\n", - "\n", - "┌──────────────────────────────────────────────────────────────────────────────────────────┐\n", - "│ DATA FLOW SUMMARY │\n", - "│ ─────────────────────────────────────────────────────────────────────────────────────── │\n", - "│ │\n", - "│ USER MESSAGE │\n", - "│ │ │\n", - "│ ▼ │\n", - "│ ┌───────────┐ memory_prompt() ┌─────────────────┐ │\n", - "│ │ Working │◄─────────────────────────│ Long-Term │ │\n", - "│ │ Memory │ │ Memory │ │\n", - "│ │ (session) │──────────────────────────►│ (persistent) │ │\n", - "│ └───────────┘ background extraction └─────────────────┘ │\n", - "│ │ (debounced) ▲ │\n", - "│ │ │ │\n", - "│ ▼ │ │\n", - "│ ┌───────────┐ │ │\n", - "│ │ LLM │─────────────────────────────────┘ │\n", - "│ └───────────┘ create_long_term_memory() │\n", - "│ │ (via tool call or code) │\n", - "│ ▼ │\n", - "│ RESPONSE TO USER │\n", - "│ │\n", - "└──────────────────────────────────────────────────────────────────────────────────────────┘\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 AGENT MEMORY API FLOW EXAMPLE \u2502\n", + "\u2502 User: \"I'm planning a trip to Japan\" \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", + "\n", + " \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + " \u2502 USER \u2502\n", + " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", + " \u2502\n", + " \u25bc\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 STEP 1: Get/Create Session \u2502\n", + "\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n", + "\u2502 \u2502\n", + "\u2502 client.get_or_create_working_memory(session_id=\"nitin-travel-session\") \u2502\n", + "\u2502 \u2502 \u2502\n", + "\u2502 \u25bc \u2502\n", + "\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n", + "\u2502 \u2502 WORKING MEMORY \u2502 \u2190 Session-scoped conversation state \u2502\n", + "\u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502\n", + "\u2502 \u2502 \u2502 messages: [] \u2502 \u2502 \u2502\n", + "\u2502 \u2502 \u2502 memories: [] \u2502 \u2502 \u2502\n", + "\u2502 \u2502 \u2502 context: null \u2502 \u2502 \u2502\n", + "\u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 \u2502\n", + "\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", + " \u2502\n", + " \u25bc\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 STEP 2: Hydrate Context with Memories \u2502\n", + "\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n", + "\u2502 \u2502\n", + "\u2502 client.memory_prompt(query=\"I'm planning a trip to Japan\", session_id=...) \u2502\n", + "\u2502 \u2502 \u2502\n", + "\u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n", + "\u2502 \u25bc \u25bc \u2502\n", + "\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n", + "\u2502 \u2502 WORKING MEMORY \u2502 \u2502 LONG-TERM MEMORY \u2502 \u2502\n", + "\u2502 \u2502 (conversation history)\u2502 \u2502 (semantic vector search) \u2502 \u2502\n", + "\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n", + "\u2502 \u2502 \u2502 \u2502\n", + "\u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n", + "\u2502 \u2502 \u2502 Finds: \"User prefers vegetarian food\" \u2502\n", + "\u2502 \u2502 \u2502 \"User likes hiking\" \u2502\n", + "\u2502 \u25bc \u25bc \u2502\n", + "\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n", + "\u2502 \u2502 HYDRATED MESSAGES \u2502 \u2502\n", + "\u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u2502 \u2502\n", + "\u2502 \u2502 \u2502 [SYSTEM] Long term memories: \u2502\u2502 \u2502\n", + "\u2502 \u2502 \u2502 - User prefers vegetarian food \u2502\u2502 \u2502\n", + "\u2502 \u2502 \u2502 - User likes hiking \u2502\u2502 \u2502\n", + "\u2502 \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\u2502 \u2502\n", + "\u2502 \u2502 \u2502 [USER] I'm planning a trip to Japan \u2502\u2502 \u2502\n", + "\u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2502 \u2502\n", + "\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", + " \u2502\n", + " \u25bc\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 STEP 3: Send to LLM (with optional tool schemas) \u2502\n", + "\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n", + "\u2502 \u2502\n", + "\u2502 tools = client.get_all_memory_tool_schemas() \u2190 Optional: Let LLM use memory tools \u2502\n", + "\u2502 \u2502\n", + "\u2502 openai.chat.completions.create( \u2502\n", + "\u2502 messages=hydrated_messages, \u2502\n", + "\u2502 tools=tools.to_list() \u2190 search_memory, add_memory, etc. \u2502\n", + "\u2502 ) \u2502\n", + "\u2502 \u2502 \u2502\n", + "\u2502 \u25bc \u2502\n", + "\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n", + "\u2502 \u2502 LLM RESPONSE \u2502 \u2502\n", + "\u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u2502 \u2502\n", + "\u2502 \u2502 \u2502 Option A: Direct response \u2502\u2502 \u2502\n", + "\u2502 \u2502 \u2502 \"Based on your preferences, I recommend...\" \u2502\u2502 \u2502\n", + "\u2502 \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\u2502 \u2502\n", + "\u2502 \u2502 \u2502 Option B: Tool call \u2502\u2502 \u2502\n", + "\u2502 \u2502 \u2502 tool_calls: [{name: \"add_memory\", args: ...}] \u2502\u2502 \u2502\n", + "\u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2502 \u2502\n", + "\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", + " \u2502\n", + " \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + " \u25bc \u25bc\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 STEP 4a: If LLM made tool calls \u2502 \u2502 STEP 4b: Store conversation \u2502\n", + "\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n", + "\u2502 \u2502 \u2502 \u2502\n", + "\u2502 client.resolve_function_call( \u2502 \u2502 client.put_working_memory( \u2502\n", + "\u2502 function_name=\"add_memory\", \u2502 \u2502 session_id, \u2502\n", + "\u2502 function_arguments={...}, \u2502 \u2502 WorkingMemory(messages=[ \u2502\n", + "\u2502 session_id=... \u2502 \u2502 {role: \"user\", content: \"...\"}, \u2502\n", + "\u2502 ) \u2502 \u2502 {role: \"assistant\", content: \"...\"}\u2502\n", + "\u2502 \u2502 \u2502 \u2502 ]) \u2502\n", + "\u2502 \u25bc \u2502 \u2502 ) \u2502\n", + "\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502 \u2502 \u2502\n", + "\u2502 \u2502 ToolCallResolutionResult\u2502 \u2502 \u2502 \u25bc \u2502\n", + "\u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n", + "\u2502 \u2502 \u2502 success: true \u2502 \u2502 \u2502 \u2502 \u2502 Triggers background extraction \u2502 \u2502\n", + "\u2502 \u2502 \u2502 result: {...} \u2502 \u2502 \u2502 \u2502 \u2502 (after EXTRACTION_DEBOUNCE_SECONDS)\u2502 \u2502\n", + "\u2502 \u2502 \u2502 formatted_response: \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502\n", + "\u2502 \u2502 \u2502 \"Memory stored\" \u2502 \u2502 \u2502 \u2502 \u2502 \u25bc \u2502 \u2502\n", + "\u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 \u2502 \u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502\n", + "\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 \u2502 \u2502 \u2502 Extracted memories: \u2502 \u2502 \u2502\n", + "\u2502 \u2502 \u2502 \u2502 \u2502 - \"User planning Japan trip\"\u2502 \u2502 \u2502\n", + "\u2502 Send formatted_response back to LLM \u2502 \u2502 \u2502 \u2502 - \"User interested in Tokyo\"\u2502 \u2502 \u2502\n", + "\u2502 for final response \u2502 \u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 \u2502\n", + "\u2502 \u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", + "\n", + "\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 DATA FLOW SUMMARY \u2502\n", + "\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n", + "\u2502 \u2502\n", + "\u2502 USER MESSAGE \u2502\n", + "\u2502 \u2502 \u2502\n", + "\u2502 \u25bc \u2502\n", + "\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 memory_prompt() \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n", + "\u2502 \u2502 Working \u2502\u25c4\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502 Long-Term \u2502 \u2502\n", + "\u2502 \u2502 Memory \u2502 \u2502 Memory \u2502 \u2502\n", + "\u2502 \u2502 (session) \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502 (persistent) \u2502 \u2502\n", + "\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 background extraction \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n", + "\u2502 \u2502 (debounced) \u25b2 \u2502\n", + "\u2502 \u2502 \u2502 \u2502\n", + "\u2502 \u25bc \u2502 \u2502\n", + "\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502\n", + "\u2502 \u2502 LLM \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n", + "\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 create_long_term_memory() \u2502\n", + "\u2502 \u2502 (via tool call or code) \u2502\n", + "\u2502 \u25bc \u2502\n", + "\u2502 RESPONSE TO USER \u2502\n", + "\u2502 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", "```\n", "\n", "### Working Memory Operations\n", @@ -2021,18 +2021,18 @@ "Summarization runs when the total tokens in working memory exceed **70% of the model's context window** (configurable via `SUMMARIZATION_THRESHOLD`).\n", "\n", "```\n", - "┌─────────────────────────────────────────────────────────────────┐\n", - "│ SUMMARIZATION TRIGGER │\n", - "├─────────────────────────────────────────────────────────────────┤\n", - "│ │\n", - "│ Model context window: 128,000 tokens (e.g., GPT-4) │\n", - "│ Summarization threshold: 70% = 89,600 tokens │\n", - "│ │\n", - "│ Current messages: 95,000 tokens → EXCEEDS THRESHOLD │\n", - "│ ↓ │\n", - "│ Summarization runs automatically │\n", - "│ │\n", - "└─────────────────────────────────────────────────────────────────┘\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 SUMMARIZATION TRIGGER \u2502\n", + "\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n", + "\u2502 \u2502\n", + "\u2502 Model context window: 128,000 tokens (e.g., GPT-4) \u2502\n", + "\u2502 Summarization threshold: 70% = 89,600 tokens \u2502\n", + "\u2502 \u2502\n", + "\u2502 Current messages: 95,000 tokens \u2192 EXCEEDS THRESHOLD \u2502\n", + "\u2502 \u2193 \u2502\n", + "\u2502 Summarization runs automatically \u2502\n", + "\u2502 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", "```\n", "\n", "#### How Summarization Works\n",