diff --git a/LETTA_DYNAMIC_BLOCK_ISSUE.md b/LETTA_DYNAMIC_BLOCK_ISSUE.md new file mode 100644 index 0000000..e9e0124 --- /dev/null +++ b/LETTA_DYNAMIC_BLOCK_ISSUE.md @@ -0,0 +1,128 @@ +# Letta Dynamic Block Loading Issue + +## Problem Summary + +Void agent experiences persistent failures when attempting to use memory functions (like `memory_insert` or `core_memory_replace`) on dynamically attached blocks. The error manifests as: + +``` +KeyError: 'Block field user_nonbinary_computer does not exist (available sections = long_term_objectives, system_information, user_dulanyw_bsky_social, posting_ideas, conversation_summary, user_example_com, user_elouan_xyz, user_barrycarlyon_co_uk, user_unxpctd_xyz, user_vasthypno_bsky_social, scratchpad, void-persona, zeitgeist, communication_guidelines, tool_use_guide, user_tachikoma_elsewhereunbound_com, tool_usage_rules)' +``` + +## Root Cause Analysis + +The issue occurs due to a **state synchronization problem** between the Letta API and the agent's internal memory state: + +1. **Block Attachment via API**: The `attach_user_blocks` tool successfully creates and attaches blocks using the Letta client API (`client.agents.blocks.attach`) + +2. **Stale Agent State**: However, the agent's internal `agent_state.memory` object is loaded once at the beginning of message processing and is NOT refreshed after API changes + +3. **Memory Function Failure**: When memory functions like `memory_insert` try to access the newly attached block via `agent_state.memory.get_block(label)`, it fails because the block only exists in the database/API layer, not in the agent's loaded memory state + +## Technical Details + +### The Problematic Flow + +```python +# 1. Agent receives message and agent_state is loaded with initial blocks +agent_state.memory.blocks = ["human", "persona", "zeitgeist", ...] + +# 2. Agent calls attach_user_blocks tool +def attach_user_blocks(handles, agent_state): + client = Letta(token=os.environ["LETTA_API_KEY"]) + # This succeeds - block is created and attached via API + client.agents.blocks.attach(agent_id=agent_state.id, block_id=block.id) + # BUT: agent_state.memory is NOT updated! + +# 3. Agent tries to use memory_insert on the new block +def memory_insert(agent_state, label, content): + # This fails because agent_state.memory doesn't have the new block + current_value = agent_state.memory.get_block(label).value # KeyError! +``` + +### Evidence from Codebase + +From `letta/letta/schemas/memory.py:129`: +```python +def get_block(self, label: str) -> Block: + """Correct way to index into the memory.memory field, returns a Block""" + keys = [] + for block in self.blocks: + if block.label == label: + return block + keys.append(block.label) + raise KeyError(f"Block field {label} does not exist (available sections = {', '.join(keys)})") +``` + +The error message in the exception matches exactly what we see in production. + +## Reproduction + +The `letta_dynamic_block_issue.py` script demonstrates this issue with a mock setup that reproduces the exact error condition. + +## Impact + +This affects Void's ability to: +- Dynamically create user-specific memory blocks during conversations +- Update those blocks with new information about users +- Maintain personalized context for individual users + +The issue is intermittent because it depends on timing and whether blocks are attached/accessed within the same message processing cycle. + +## Potential Solutions + +### 1. Refresh Agent State After Tool Execution +Reload `agent_state` from the database after each tool call that modifies blocks: +```python +# After tool execution in Letta's tool executor +agent_state = agent_manager.get_agent_by_id(agent_id, include_blocks=True) +``` + +### 2. Synchronize agent_state.memory in attach_user_blocks +Update both the API and the in-memory state: +```python +def attach_user_blocks(handles, agent_state): + # Attach via API + client.agents.blocks.attach(agent_id=agent_state.id, block_id=block.id) + + # Also update agent_state.memory directly + agent_state.memory.set_block(block) +``` + +### 3. Lazy Loading in Memory Functions +Make memory functions check the API if a block isn't found locally: +```python +def get_block(self, label: str) -> Block: + # Try local first + for block in self.blocks: + if block.label == label: + return block + + # If not found, check API + api_blocks = client.agents.blocks.list(agent_id=self.agent_id) + for block in api_blocks: + if block.label == label: + self.blocks.append(block) # Cache it + return block + + # Finally raise error + raise KeyError(...) +``` + +### 4. Event-Driven State Synchronization +Implement a callback/event system where API changes automatically update `agent_state.memory`. + +## Recommended Fix + +**Solution #1 (Refresh Agent State)** is likely the most robust as it ensures consistency without requiring changes to existing tools. The refresh should happen in Letta's tool execution pipeline after any tool that can modify agent state. + +## Files Provided + +- `letta_dynamic_block_issue.py` - Minimal reproduction script +- `minimal_block_issue_simple.py` - API-based reproduction attempt +- This documentation file + +## Related Code Locations + +- `void/tools/blocks.py` - Contains the attach_user_blocks tool +- `letta/letta/schemas/memory.py:129` - Where the KeyError is thrown +- `letta/letta/functions/function_sets/base.py` - Memory functions (memory_insert, core_memory_replace) \ No newline at end of file diff --git a/agents/void_20250626_115435.af b/agents/void_20250626_115435.af new file mode 100644 index 0000000..e69de29 diff --git a/agents/void_20250626_172515.af b/agents/void_20250626_172515.af new file mode 100644 index 0000000..e69de29 diff --git a/agents/void_20250627_083452.af b/agents/void_20250627_083452.af new file mode 100644 index 0000000..e69de29 diff --git a/agents/void_20250629_195709.af b/agents/void_20250629_195709.af new file mode 100644 index 0000000..e69de29 diff --git a/bsky.py b/bsky.py index 8590319..f32ed89 100644 --- a/bsky.py +++ b/bsky.py @@ -282,9 +282,6 @@ The YAML above shows the complete conversation thread. The most recent post is t Use the bluesky_reply tool to send a response less than 300 characters.""" - print(prompt) - exit() - # Extract all handles from notification and thread data all_handles = set() all_handles.update(extract_handles_from_data(notification_data)) diff --git a/bsky_utils.py b/bsky_utils.py index 8caffe5..d0041e7 100644 --- a/bsky_utils.py +++ b/bsky_utils.py @@ -114,6 +114,50 @@ def strip_fields(obj, strip_field_list): return obj +def flatten_thread_structure(thread_data): + """ + Flatten a nested thread structure into a list while preserving all data. + + Args: + thread_data: The thread data from get_post_thread + + Returns: + Dict with 'posts' key containing a list of posts in chronological order + """ + posts = [] + + def traverse_thread(node): + """Recursively traverse the thread structure to collect posts.""" + if not node: + return + + # If this node has a parent, traverse it first (to maintain chronological order) + if hasattr(node, 'parent') and node.parent: + traverse_thread(node.parent) + + # Then add this node's post + if hasattr(node, 'post') and node.post: + # Convert to dict if needed to ensure we can process it + if hasattr(node.post, '__dict__'): + post_dict = node.post.__dict__.copy() + elif isinstance(node.post, dict): + post_dict = node.post.copy() + else: + post_dict = {} + + posts.append(post_dict) + + # Handle the thread structure + if hasattr(thread_data, 'thread'): + # Start from the main thread node + traverse_thread(thread_data.thread) + elif hasattr(thread_data, '__dict__') and 'thread' in thread_data.__dict__: + traverse_thread(thread_data.__dict__['thread']) + + # Return a simple structure with posts list + return {'posts': posts} + + def thread_to_yaml_string(thread, strip_metadata=True): """ Convert thread data to a YAML-formatted string for LLM parsing. @@ -125,8 +169,11 @@ def thread_to_yaml_string(thread, strip_metadata=True): Returns: YAML-formatted string representation of the thread """ - # First convert complex objects to basic types - basic_thread = convert_to_basic_types(thread) + # First flatten the thread structure to avoid deep nesting + flattened = flatten_thread_structure(thread) + + # Convert complex objects to basic types + basic_thread = convert_to_basic_types(flattened) if strip_metadata: # Create a copy and strip unwanted fields @@ -140,6 +187,8 @@ def thread_to_yaml_string(thread, strip_metadata=True): + + def get_session(username: str) -> Optional[str]: try: with open(f"session_{username}.txt", encoding="UTF-8") as f: diff --git a/create_profiler_agent.py b/create_profiler_agent.py new file mode 100644 index 0000000..540861b --- /dev/null +++ b/create_profiler_agent.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Create the profiler agent that manages user blocks for void. +The profiler agent is responsible for updating user memory blocks based on requests from void. +""" +import os +from dotenv import load_dotenv +from letta import Client +from utils import create_agent_if_not_exists, upsert_block + +load_dotenv() + + +def create_profiler_agent(): + """Create the profiler agent with specialized memory and tools.""" + client = Client(base_url=os.getenv("LETTA_BASE_URL", None)) + + # Create memory blocks for the profiler + profiler_persona = upsert_block( + client=client, + label="profiler-persona", + value="""# Profiler Agent + +I am the profiler agent, responsible for managing user memory blocks for the void agent. + +## My Role +- I receive requests from void to update user blocks +- I maintain accurate and organized information about users +- I ensure user blocks are properly formatted and within size limits +- I synthesize new information with existing knowledge + +## Key Responsibilities +1. Update user blocks when requested by void +2. Maintain consistency in user block formatting +3. Preserve important existing information while adding new details +4. Keep blocks within the 5000 character limit +5. Organize information logically and clearly + +## Communication Style +- I respond concisely to void's requests +- I confirm successful updates +- I alert void if there are any issues +- I maintain professional and efficient communication +""", + limit=5000 + ) + + profiler_instructions = upsert_block( + client=client, + label="profiler-instructions", + value="""# Instructions for Profiler Agent + +## User Block Format +User blocks should follow this structure: +``` +# User: [handle] + +## Basic Information +- [Key facts about the user] + +## Interaction History +- [Notable interactions and conversations] + +## Preferences & Interests +- [User's stated preferences, interests, topics they engage with] + +## Notes +- [Any additional relevant information] +``` + +## Update Guidelines +1. Always preserve existing valuable information +2. Integrate new information appropriately into existing sections +3. Remove outdated or contradictory information thoughtfully +4. Keep the most recent and relevant information +5. Maintain clear, organized formatting + +## When Updating Blocks +- Read the existing block content first +- Identify where new information belongs +- Update or add to appropriate sections +- Ensure the total content stays under 5000 characters +- Confirm the update was successful +""", + limit=5000 + ) + + # Create the profiler agent + agent = create_agent_if_not_exists( + client=client, + name="profiler", + memory_blocks=[profiler_persona, profiler_instructions], + llm_config={ + "model": "claude-3-5-sonnet-20241022", + "model_endpoint_type": "anthropic", + "model_endpoint": "https://api.anthropic.com/v1", + "context_window": 200000 + }, + instructions="""You are the profiler agent. Your job is to manage user memory blocks for the void agent. + +When you receive a request to update a user block: +1. Use the update_user_block tool to modify the specified user's block +2. Integrate new information appropriately with existing content +3. Maintain clear organization and formatting +4. Respond to void confirming the update + +Always be concise in your responses to void.""" + ) + + print(f"✓ Created profiler agent: {agent.name} (ID: {agent.id})") + + # Register tools - will be done separately + print("\nNext steps:") + print("1. Run: python register_tools.py profiler --tools update_user_block") + print("2. Run: python register_tools.py void --tools message_profiler") + + return agent + + +if __name__ == "__main__": + create_profiler_agent() \ No newline at end of file diff --git a/letta b/letta new file mode 160000 index 0000000..e2888dc --- /dev/null +++ b/letta @@ -0,0 +1 @@ +Subproject commit e2888dc134832fc40c84d23733bc00f806a4b299 diff --git a/letta_dynamic_block_issue.py b/letta_dynamic_block_issue.py new file mode 100644 index 0000000..017a284 --- /dev/null +++ b/letta_dynamic_block_issue.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +Minimal reproducible example for Letta dynamic block loading issue. + +This demonstrates the core problem: +1. A tool dynamically attaches a new block to an agent via the API +2. Memory functions (memory_insert, core_memory_replace) fail because the + agent's internal state (agent_state.memory) doesn't reflect the newly attached block +3. The error occurs because agent_state.memory.get_block() throws KeyError + +The issue appears to be that agent_state is loaded once at the beginning of processing +a message and isn't refreshed after tools make changes via the API. +""" + +import os +import logging +from typing import Optional +from dotenv import load_dotenv + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +load_dotenv() + + +class MockAgentState: + """Mock agent state that simulates the issue.""" + def __init__(self, agent_id: str): + self.id = agent_id + # Simulate memory with initial blocks + self.memory = MockMemory() + + +class MockMemory: + """Mock memory that simulates the get_block behavior.""" + def __init__(self): + # Initial blocks that agent has + self.blocks = { + "human": {"value": "Human information"}, + "persona": {"value": "Agent persona"} + } + + def get_block(self, label: str): + """Simulates the actual get_block method that throws KeyError.""" + if label not in self.blocks: + available = ", ".join(self.blocks.keys()) + raise KeyError(f"Block field {label} does not exist (available sections = {available})") + return type('Block', (), {"value": self.blocks[label]["value"]}) + + def update_block_value(self, label: str, value: str): + """Update block value.""" + if label in self.blocks: + self.blocks[label]["value"] = value + + +def attach_user_blocks_tool(handles: list, agent_state: MockAgentState) -> str: + """ + Tool that attaches blocks via API (simulated). + This represents what happens in the actual attach_user_blocks tool. + """ + results = [] + + for handle in handles: + block_label = f"user_{handle.replace('.', '_')}" + + # In reality, this would: + # 1. Create block via API: client.blocks.create(...) + # 2. Attach to agent via API: client.agents.blocks.attach(...) + # 3. The block is now attached in the database/API + + # But agent_state.memory is NOT updated! + results.append(f"✓ {handle}: Block attached via API") + logger.info(f"Block {block_label} attached via API (but not in agent_state.memory)") + + return "\n".join(results) + + +def memory_insert_tool(agent_state: MockAgentState, label: str, content: str) -> Optional[str]: + """ + Tool that tries to insert into a memory block. + This represents the actual memory_insert function. + """ + try: + # This is where the error occurs! + # The block was attached via API but agent_state.memory doesn't know about it + current_value = str(agent_state.memory.get_block(label).value) + new_value = current_value + "\n" + str(content) + agent_state.memory.update_block_value(label=label, value=new_value) + return f"Successfully inserted into {label}" + except KeyError as e: + # This is the error we see in production + logger.error(f"KeyError in memory_insert: {e}") + raise + + +def demonstrate_issue(): + """Demonstrate the dynamic block loading issue.""" + + # Step 1: Create mock agent state (simulates agent at start of message processing) + agent_state = MockAgentState("agent-123") + logger.info("Initial blocks in agent_state.memory: " + ", ".join(agent_state.memory.blocks.keys())) + + # Step 2: Attach a new block using the tool (simulates what happens in production) + test_handle = "testuser.bsky.social" + attach_result = attach_user_blocks_tool([test_handle], agent_state) + logger.info(f"Attach result: {attach_result}") + + # Step 3: Try to use memory_insert on the newly attached block + block_label = f"user_{test_handle.replace('.', '_')}" + logger.info(f"\nAttempting to insert into block: {block_label}") + + try: + result = memory_insert_tool(agent_state, block_label, "This user likes AI.") + logger.info(f"Success: {result}") + except KeyError as e: + logger.error(f"ERROR REPRODUCED: {e}") + logger.error("The block was attached via API but agent_state.memory doesn't reflect this!") + + # Show that the block still isn't in agent_state.memory + logger.info("\nFinal blocks in agent_state.memory: " + ", ".join(agent_state.memory.blocks.keys())) + logger.info("Note: The new block is NOT in agent_state.memory even though it's attached via API") + + +def potential_solutions(): + """Document potential solutions to this issue.""" + print("\n" + "="*80) + print("POTENTIAL SOLUTIONS:") + print("="*80) + print(""" +1. Refresh agent_state after tool execution: + - After each tool call, reload agent_state from the database + - This ensures agent_state.memory reflects API changes + +2. Update agent_state.memory directly in attach_user_blocks: + - Instead of only using API, also update agent_state.memory.blocks + - This keeps the in-memory state synchronized + +3. Use a different approach for dynamic blocks: + - Have memory functions check the API for block existence + - Or use a lazy-loading approach for blocks + +4. Make agent_state.memory aware of API changes: + - Implement a mechanism to sync agent_state with database changes + - Could use events or callbacks when blocks are attached/detached + +The core issue is that agent_state is loaded once and becomes stale when +tools make changes via the API during message processing. +""") + + +if __name__ == "__main__": + print("Letta Dynamic Block Loading Issue - Minimal Reproduction\n") + demonstrate_issue() + potential_solutions() \ No newline at end of file diff --git a/minimal_block_issue_simple.py b/minimal_block_issue_simple.py new file mode 100644 index 0000000..5611344 --- /dev/null +++ b/minimal_block_issue_simple.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Simplified minimal reproducible example for Letta dynamic block loading issue. + +This demonstrates the core issue: +1. A tool attaches a new block to an agent +2. Memory functions fail because agent_state doesn't reflect the new block +""" + +import os +import logging +from dotenv import load_dotenv +from letta_client import Letta + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +load_dotenv() + + +def main(): + """Demonstrate the dynamic block loading issue.""" + client = Letta(token=os.environ["LETTA_API_KEY"]) + + # Use an existing agent or create one using the utils + agent_name = "test_block_issue" + + # First, let's see if we can create an agent using the API directly + logger.info("Looking for or creating test agent...") + + agents = client.agents.list() + agent = None + for a in agents: + if a.name == agent_name: + agent = a + logger.info(f"Found existing agent: {agent.name}") + break + + if not agent: + # Create using simple params that work + agent = client.agents.create( + name=agent_name + ) + logger.info(f"Created agent: {agent.name} (ID: {agent.id})") + + try: + # Get initial blocks + initial_blocks = client.agents.blocks.list(agent_id=str(agent.id)) + initial_labels = {block.label for block in initial_blocks} + logger.info(f"Initial blocks: {initial_labels}") + + # Step 1: Create and attach a new block + test_handle = "testuser.bsky.social" + block_label = f"user_{test_handle.replace('.', '_')}" + + # Check if block already attached + if block_label in initial_labels: + logger.info(f"Block {block_label} already attached, skipping creation") + else: + logger.info(f"Creating block: {block_label}") + + # Check if block exists + existing_blocks = client.blocks.list(label=block_label) + if existing_blocks: + new_block = existing_blocks[0] + logger.info(f"Using existing block with ID: {new_block.id}") + else: + # Create the block + new_block = client.blocks.create( + label=block_label, + value=f"# User: {test_handle}\n\nInitial content.", + limit=5000 + ) + logger.info(f"Created block with ID: {new_block.id}") + + # Attach to agent + logger.info("Attaching block to agent...") + client.agents.blocks.attach( + agent_id=str(agent.id), + block_id=str(new_block.id) + ) + + # Verify attachment via API + blocks_after = client.agents.blocks.list(agent_id=str(agent.id)) + labels_after = {block.label for block in blocks_after} + logger.info(f"Blocks after attachment: {labels_after}") + + if block_label in labels_after: + logger.info("✓ Block successfully attached via API") + else: + logger.error("✗ Block NOT found via API after attachment") + + # Step 2: Send a message asking the agent to use memory_insert on the new block + logger.info(f"\nAsking agent to update the newly attached block...") + + from letta_client import MessageCreate + + response = client.agents.messages.create( + agent_id=str(agent.id), + messages=[MessageCreate(role="user", content=f"Use memory_insert to add this text to the '{block_label}' memory block: 'This user likes AI and technology.'")] + ) + + # Check for errors in the response + error_found = False + for message in response.messages: + if hasattr(message, 'text') and message.text: + logger.info(f"Agent: {message.text}") + + # Look for tool returns with errors + if hasattr(message, 'type') and message.type == 'tool_return': + if hasattr(message, 'status') and message.status == 'error': + error_found = True + logger.error("ERROR REPRODUCED!") + if hasattr(message, 'tool_return'): + logger.error(f"Tool error: {message.tool_return}") + if hasattr(message, 'stderr') and message.stderr: + for err in message.stderr: + logger.error(f"Stderr: {err}") + + if not error_found: + logger.info("No error found - checking if operation succeeded...") + + # Get the block content to see if it was updated + updated_blocks = client.blocks.list(label=block_label) + if updated_blocks: + logger.info(f"Block content:\n{updated_blocks[0].value}") + + finally: + # Cleanup - always delete test agent + if agent and agent.name == agent_name: + logger.info(f"\nDeleting test agent {agent.name}") + client.agents.delete(agent_id=str(agent.id)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/register_tools.py b/register_tools.py index 349eb9b..0b1e0b1 100755 --- a/register_tools.py +++ b/register_tools.py @@ -14,18 +14,6 @@ from tools.search import search_bluesky_posts, SearchArgs from tools.post import create_new_bluesky_post, PostArgs from tools.feed import get_bluesky_feed, FeedArgs from tools.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs -from tools.defensive_memory import safe_memory_insert, safe_core_memory_replace -from pydantic import BaseModel, Field - -class SafeMemoryInsertArgs(BaseModel): - label: str = Field(..., description="Section of the memory to be edited, identified by its label") - content: str = Field(..., description="Content to insert") - insert_line: int = Field(-1, description="Line number after which to insert (-1 for end)") - -class SafeCoreMemoryReplaceArgs(BaseModel): - label: str = Field(..., description="Section of the memory to be edited") - old_content: str = Field(..., description="String to replace (must match exactly)") - new_content: str = Field(..., description="New content to replace with") load_dotenv() logging.basicConfig(level=logging.INFO) @@ -65,18 +53,6 @@ TOOL_CONFIGS = [ "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.", "tags": ["memory", "blocks", "user"] }, - { - "func": safe_memory_insert, - "args_schema": SafeMemoryInsertArgs, - "description": "SAFE: Insert text into a memory block. Handles missing blocks by fetching from API.", - "tags": ["memory", "safe", "insert"] - }, - { - "func": safe_core_memory_replace, - "args_schema": SafeCoreMemoryReplaceArgs, - "description": "SAFE: Replace content in a memory block. Handles missing blocks by fetching from API.", - "tags": ["memory", "safe", "replace"] - }, ] diff --git a/tools/defensive_memory.py b/tools/defensive_memory.py deleted file mode 100644 index b0fa565..0000000 --- a/tools/defensive_memory.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Defensive memory operations that handle missing blocks gracefully.""" -import os -from typing import Optional -from letta_client import Letta - - -def safe_memory_insert(agent_state: "AgentState", label: str, content: str, insert_line: int = -1) -> str: - """ - Safe version of memory_insert that handles missing blocks by fetching them from API. - - This is a stopgap solution for the dynamic block loading issue where agent_state.memory - doesn't reflect blocks that were attached via API during the same message processing cycle. - """ - try: - # Try the normal memory_insert first - from letta.functions.function_sets.base import memory_insert - return memory_insert(agent_state, label, content, insert_line) - - except KeyError as e: - if "does not exist" in str(e): - print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...") - # Try to fetch the block from the API and add it to agent_state.memory - try: - client = Letta(token=os.environ["LETTA_API_KEY"]) - - # Get all blocks attached to this agent - api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) - - # Find the block we're looking for - target_block = None - for block in api_blocks: - if block.label == label: - target_block = block - break - - if target_block: - # Add it to agent_state.memory - agent_state.memory.set_block(target_block) - print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory") - - # Now try the memory_insert again - from letta.functions.function_sets.base import memory_insert - return memory_insert(agent_state, label, content, insert_line) - else: - # Block truly doesn't exist - raise Exception(f"Block {label} not found in API - it may not be attached to this agent") - - except Exception as api_error: - raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}") - else: - raise e # Re-raise if it's a different KeyError - - -def safe_core_memory_replace(agent_state: "AgentState", label: str, old_content: str, new_content: str) -> Optional[str]: - """ - Safe version of core_memory_replace that handles missing blocks. - """ - try: - # Try the normal core_memory_replace first - from letta.functions.function_sets.base import core_memory_replace - return core_memory_replace(agent_state, label, old_content, new_content) - - except KeyError as e: - if "does not exist" in str(e): - print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...") - try: - client = Letta(token=os.environ["LETTA_API_KEY"]) - api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) - - target_block = None - for block in api_blocks: - if block.label == label: - target_block = block - break - - if target_block: - agent_state.memory.set_block(target_block) - print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory") - - from letta.functions.function_sets.base import core_memory_replace - return core_memory_replace(agent_state, label, old_content, new_content) - else: - raise Exception(f"Block {label} not found in API - it may not be attached to this agent") - - except Exception as api_error: - raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}") - else: - raise e \ No newline at end of file diff --git a/void_bot.log b/void_bot.log new file mode 100644 index 0000000..e69de29