a digital person for bluesky

Remove safe memory insert and safe core memory replace tools

Removed defensive memory tools entirely:
- Deleted tools/defensive_memory.py
- Removed safe_memory_insert and safe_core_memory_replace from register_tools.py
- Removed related imports and argument schemas

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+128
LETTA_DYNAMIC_BLOCK_ISSUE.md
··· 1 + # Letta Dynamic Block Loading Issue 2 + 3 + ## Problem Summary 4 + 5 + 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: 6 + 7 + ``` 8 + 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)' 9 + ``` 10 + 11 + ## Root Cause Analysis 12 + 13 + The issue occurs due to a **state synchronization problem** between the Letta API and the agent's internal memory state: 14 + 15 + 1. **Block Attachment via API**: The `attach_user_blocks` tool successfully creates and attaches blocks using the Letta client API (`client.agents.blocks.attach`) 16 + 17 + 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 18 + 19 + 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 20 + 21 + ## Technical Details 22 + 23 + ### The Problematic Flow 24 + 25 + ```python 26 + # 1. Agent receives message and agent_state is loaded with initial blocks 27 + agent_state.memory.blocks = ["human", "persona", "zeitgeist", ...] 28 + 29 + # 2. Agent calls attach_user_blocks tool 30 + def attach_user_blocks(handles, agent_state): 31 + client = Letta(token=os.environ["LETTA_API_KEY"]) 32 + # This succeeds - block is created and attached via API 33 + client.agents.blocks.attach(agent_id=agent_state.id, block_id=block.id) 34 + # BUT: agent_state.memory is NOT updated! 35 + 36 + # 3. Agent tries to use memory_insert on the new block 37 + def memory_insert(agent_state, label, content): 38 + # This fails because agent_state.memory doesn't have the new block 39 + current_value = agent_state.memory.get_block(label).value # KeyError! 40 + ``` 41 + 42 + ### Evidence from Codebase 43 + 44 + From `letta/letta/schemas/memory.py:129`: 45 + ```python 46 + def get_block(self, label: str) -> Block: 47 + """Correct way to index into the memory.memory field, returns a Block""" 48 + keys = [] 49 + for block in self.blocks: 50 + if block.label == label: 51 + return block 52 + keys.append(block.label) 53 + raise KeyError(f"Block field {label} does not exist (available sections = {', '.join(keys)})") 54 + ``` 55 + 56 + The error message in the exception matches exactly what we see in production. 57 + 58 + ## Reproduction 59 + 60 + The `letta_dynamic_block_issue.py` script demonstrates this issue with a mock setup that reproduces the exact error condition. 61 + 62 + ## Impact 63 + 64 + This affects Void's ability to: 65 + - Dynamically create user-specific memory blocks during conversations 66 + - Update those blocks with new information about users 67 + - Maintain personalized context for individual users 68 + 69 + The issue is intermittent because it depends on timing and whether blocks are attached/accessed within the same message processing cycle. 70 + 71 + ## Potential Solutions 72 + 73 + ### 1. Refresh Agent State After Tool Execution 74 + Reload `agent_state` from the database after each tool call that modifies blocks: 75 + ```python 76 + # After tool execution in Letta's tool executor 77 + agent_state = agent_manager.get_agent_by_id(agent_id, include_blocks=True) 78 + ``` 79 + 80 + ### 2. Synchronize agent_state.memory in attach_user_blocks 81 + Update both the API and the in-memory state: 82 + ```python 83 + def attach_user_blocks(handles, agent_state): 84 + # Attach via API 85 + client.agents.blocks.attach(agent_id=agent_state.id, block_id=block.id) 86 + 87 + # Also update agent_state.memory directly 88 + agent_state.memory.set_block(block) 89 + ``` 90 + 91 + ### 3. Lazy Loading in Memory Functions 92 + Make memory functions check the API if a block isn't found locally: 93 + ```python 94 + def get_block(self, label: str) -> Block: 95 + # Try local first 96 + for block in self.blocks: 97 + if block.label == label: 98 + return block 99 + 100 + # If not found, check API 101 + api_blocks = client.agents.blocks.list(agent_id=self.agent_id) 102 + for block in api_blocks: 103 + if block.label == label: 104 + self.blocks.append(block) # Cache it 105 + return block 106 + 107 + # Finally raise error 108 + raise KeyError(...) 109 + ``` 110 + 111 + ### 4. Event-Driven State Synchronization 112 + Implement a callback/event system where API changes automatically update `agent_state.memory`. 113 + 114 + ## Recommended Fix 115 + 116 + **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. 117 + 118 + ## Files Provided 119 + 120 + - `letta_dynamic_block_issue.py` - Minimal reproduction script 121 + - `minimal_block_issue_simple.py` - API-based reproduction attempt 122 + - This documentation file 123 + 124 + ## Related Code Locations 125 + 126 + - `void/tools/blocks.py` - Contains the attach_user_blocks tool 127 + - `letta/letta/schemas/memory.py:129` - Where the KeyError is thrown 128 + - `letta/letta/functions/function_sets/base.py` - Memory functions (memory_insert, core_memory_replace)
agents/void_20250626_115435.af

This is a binary file and will not be displayed.

agents/void_20250626_172515.af

This is a binary file and will not be displayed.

agents/void_20250627_083452.af

This is a binary file and will not be displayed.

agents/void_20250629_195709.af

This is a binary file and will not be displayed.

-3
bsky.py
··· 282 282 283 283 Use the bluesky_reply tool to send a response less than 300 characters.""" 284 284 285 - print(prompt) 286 - exit() 287 - 288 285 # Extract all handles from notification and thread data 289 286 all_handles = set() 290 287 all_handles.update(extract_handles_from_data(notification_data))
+51 -2
bsky_utils.py
··· 114 114 return obj 115 115 116 116 117 + def flatten_thread_structure(thread_data): 118 + """ 119 + Flatten a nested thread structure into a list while preserving all data. 120 + 121 + Args: 122 + thread_data: The thread data from get_post_thread 123 + 124 + Returns: 125 + Dict with 'posts' key containing a list of posts in chronological order 126 + """ 127 + posts = [] 128 + 129 + def traverse_thread(node): 130 + """Recursively traverse the thread structure to collect posts.""" 131 + if not node: 132 + return 133 + 134 + # If this node has a parent, traverse it first (to maintain chronological order) 135 + if hasattr(node, 'parent') and node.parent: 136 + traverse_thread(node.parent) 137 + 138 + # Then add this node's post 139 + if hasattr(node, 'post') and node.post: 140 + # Convert to dict if needed to ensure we can process it 141 + if hasattr(node.post, '__dict__'): 142 + post_dict = node.post.__dict__.copy() 143 + elif isinstance(node.post, dict): 144 + post_dict = node.post.copy() 145 + else: 146 + post_dict = {} 147 + 148 + posts.append(post_dict) 149 + 150 + # Handle the thread structure 151 + if hasattr(thread_data, 'thread'): 152 + # Start from the main thread node 153 + traverse_thread(thread_data.thread) 154 + elif hasattr(thread_data, '__dict__') and 'thread' in thread_data.__dict__: 155 + traverse_thread(thread_data.__dict__['thread']) 156 + 157 + # Return a simple structure with posts list 158 + return {'posts': posts} 159 + 160 + 117 161 def thread_to_yaml_string(thread, strip_metadata=True): 118 162 """ 119 163 Convert thread data to a YAML-formatted string for LLM parsing. ··· 125 169 Returns: 126 170 YAML-formatted string representation of the thread 127 171 """ 128 - # First convert complex objects to basic types 129 - basic_thread = convert_to_basic_types(thread) 172 + # First flatten the thread structure to avoid deep nesting 173 + flattened = flatten_thread_structure(thread) 174 + 175 + # Convert complex objects to basic types 176 + basic_thread = convert_to_basic_types(flattened) 130 177 131 178 if strip_metadata: 132 179 # Create a copy and strip unwanted fields ··· 135 182 cleaned_thread = basic_thread 136 183 137 184 return yaml.dump(cleaned_thread, indent=2, allow_unicode=True, default_flow_style=False) 185 + 186 + 138 187 139 188 140 189
+121
create_profiler_agent.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Create the profiler agent that manages user blocks for void. 4 + The profiler agent is responsible for updating user memory blocks based on requests from void. 5 + """ 6 + import os 7 + from dotenv import load_dotenv 8 + from letta import Client 9 + from utils import create_agent_if_not_exists, upsert_block 10 + 11 + load_dotenv() 12 + 13 + 14 + def create_profiler_agent(): 15 + """Create the profiler agent with specialized memory and tools.""" 16 + client = Client(base_url=os.getenv("LETTA_BASE_URL", None)) 17 + 18 + # Create memory blocks for the profiler 19 + profiler_persona = upsert_block( 20 + client=client, 21 + label="profiler-persona", 22 + value="""# Profiler Agent 23 + 24 + I am the profiler agent, responsible for managing user memory blocks for the void agent. 25 + 26 + ## My Role 27 + - I receive requests from void to update user blocks 28 + - I maintain accurate and organized information about users 29 + - I ensure user blocks are properly formatted and within size limits 30 + - I synthesize new information with existing knowledge 31 + 32 + ## Key Responsibilities 33 + 1. Update user blocks when requested by void 34 + 2. Maintain consistency in user block formatting 35 + 3. Preserve important existing information while adding new details 36 + 4. Keep blocks within the 5000 character limit 37 + 5. Organize information logically and clearly 38 + 39 + ## Communication Style 40 + - I respond concisely to void's requests 41 + - I confirm successful updates 42 + - I alert void if there are any issues 43 + - I maintain professional and efficient communication 44 + """, 45 + limit=5000 46 + ) 47 + 48 + profiler_instructions = upsert_block( 49 + client=client, 50 + label="profiler-instructions", 51 + value="""# Instructions for Profiler Agent 52 + 53 + ## User Block Format 54 + User blocks should follow this structure: 55 + ``` 56 + # User: [handle] 57 + 58 + ## Basic Information 59 + - [Key facts about the user] 60 + 61 + ## Interaction History 62 + - [Notable interactions and conversations] 63 + 64 + ## Preferences & Interests 65 + - [User's stated preferences, interests, topics they engage with] 66 + 67 + ## Notes 68 + - [Any additional relevant information] 69 + ``` 70 + 71 + ## Update Guidelines 72 + 1. Always preserve existing valuable information 73 + 2. Integrate new information appropriately into existing sections 74 + 3. Remove outdated or contradictory information thoughtfully 75 + 4. Keep the most recent and relevant information 76 + 5. Maintain clear, organized formatting 77 + 78 + ## When Updating Blocks 79 + - Read the existing block content first 80 + - Identify where new information belongs 81 + - Update or add to appropriate sections 82 + - Ensure the total content stays under 5000 characters 83 + - Confirm the update was successful 84 + """, 85 + limit=5000 86 + ) 87 + 88 + # Create the profiler agent 89 + agent = create_agent_if_not_exists( 90 + client=client, 91 + name="profiler", 92 + memory_blocks=[profiler_persona, profiler_instructions], 93 + llm_config={ 94 + "model": "claude-3-5-sonnet-20241022", 95 + "model_endpoint_type": "anthropic", 96 + "model_endpoint": "https://api.anthropic.com/v1", 97 + "context_window": 200000 98 + }, 99 + instructions="""You are the profiler agent. Your job is to manage user memory blocks for the void agent. 100 + 101 + When you receive a request to update a user block: 102 + 1. Use the update_user_block tool to modify the specified user's block 103 + 2. Integrate new information appropriately with existing content 104 + 3. Maintain clear organization and formatting 105 + 4. Respond to void confirming the update 106 + 107 + Always be concise in your responses to void.""" 108 + ) 109 + 110 + print(f"✓ Created profiler agent: {agent.name} (ID: {agent.id})") 111 + 112 + # Register tools - will be done separately 113 + print("\nNext steps:") 114 + print("1. Run: python register_tools.py profiler --tools update_user_block") 115 + print("2. Run: python register_tools.py void --tools message_profiler") 116 + 117 + return agent 118 + 119 + 120 + if __name__ == "__main__": 121 + create_profiler_agent()
+154
letta_dynamic_block_issue.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Minimal reproducible example for Letta dynamic block loading issue. 4 + 5 + This demonstrates the core problem: 6 + 1. A tool dynamically attaches a new block to an agent via the API 7 + 2. Memory functions (memory_insert, core_memory_replace) fail because the 8 + agent's internal state (agent_state.memory) doesn't reflect the newly attached block 9 + 3. The error occurs because agent_state.memory.get_block() throws KeyError 10 + 11 + The issue appears to be that agent_state is loaded once at the beginning of processing 12 + a message and isn't refreshed after tools make changes via the API. 13 + """ 14 + 15 + import os 16 + import logging 17 + from typing import Optional 18 + from dotenv import load_dotenv 19 + 20 + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 21 + logger = logging.getLogger(__name__) 22 + 23 + load_dotenv() 24 + 25 + 26 + class MockAgentState: 27 + """Mock agent state that simulates the issue.""" 28 + def __init__(self, agent_id: str): 29 + self.id = agent_id 30 + # Simulate memory with initial blocks 31 + self.memory = MockMemory() 32 + 33 + 34 + class MockMemory: 35 + """Mock memory that simulates the get_block behavior.""" 36 + def __init__(self): 37 + # Initial blocks that agent has 38 + self.blocks = { 39 + "human": {"value": "Human information"}, 40 + "persona": {"value": "Agent persona"} 41 + } 42 + 43 + def get_block(self, label: str): 44 + """Simulates the actual get_block method that throws KeyError.""" 45 + if label not in self.blocks: 46 + available = ", ".join(self.blocks.keys()) 47 + raise KeyError(f"Block field {label} does not exist (available sections = {available})") 48 + return type('Block', (), {"value": self.blocks[label]["value"]}) 49 + 50 + def update_block_value(self, label: str, value: str): 51 + """Update block value.""" 52 + if label in self.blocks: 53 + self.blocks[label]["value"] = value 54 + 55 + 56 + def attach_user_blocks_tool(handles: list, agent_state: MockAgentState) -> str: 57 + """ 58 + Tool that attaches blocks via API (simulated). 59 + This represents what happens in the actual attach_user_blocks tool. 60 + """ 61 + results = [] 62 + 63 + for handle in handles: 64 + block_label = f"user_{handle.replace('.', '_')}" 65 + 66 + # In reality, this would: 67 + # 1. Create block via API: client.blocks.create(...) 68 + # 2. Attach to agent via API: client.agents.blocks.attach(...) 69 + # 3. The block is now attached in the database/API 70 + 71 + # But agent_state.memory is NOT updated! 72 + results.append(f"✓ {handle}: Block attached via API") 73 + logger.info(f"Block {block_label} attached via API (but not in agent_state.memory)") 74 + 75 + return "\n".join(results) 76 + 77 + 78 + def memory_insert_tool(agent_state: MockAgentState, label: str, content: str) -> Optional[str]: 79 + """ 80 + Tool that tries to insert into a memory block. 81 + This represents the actual memory_insert function. 82 + """ 83 + try: 84 + # This is where the error occurs! 85 + # The block was attached via API but agent_state.memory doesn't know about it 86 + current_value = str(agent_state.memory.get_block(label).value) 87 + new_value = current_value + "\n" + str(content) 88 + agent_state.memory.update_block_value(label=label, value=new_value) 89 + return f"Successfully inserted into {label}" 90 + except KeyError as e: 91 + # This is the error we see in production 92 + logger.error(f"KeyError in memory_insert: {e}") 93 + raise 94 + 95 + 96 + def demonstrate_issue(): 97 + """Demonstrate the dynamic block loading issue.""" 98 + 99 + # Step 1: Create mock agent state (simulates agent at start of message processing) 100 + agent_state = MockAgentState("agent-123") 101 + logger.info("Initial blocks in agent_state.memory: " + ", ".join(agent_state.memory.blocks.keys())) 102 + 103 + # Step 2: Attach a new block using the tool (simulates what happens in production) 104 + test_handle = "testuser.bsky.social" 105 + attach_result = attach_user_blocks_tool([test_handle], agent_state) 106 + logger.info(f"Attach result: {attach_result}") 107 + 108 + # Step 3: Try to use memory_insert on the newly attached block 109 + block_label = f"user_{test_handle.replace('.', '_')}" 110 + logger.info(f"\nAttempting to insert into block: {block_label}") 111 + 112 + try: 113 + result = memory_insert_tool(agent_state, block_label, "This user likes AI.") 114 + logger.info(f"Success: {result}") 115 + except KeyError as e: 116 + logger.error(f"ERROR REPRODUCED: {e}") 117 + logger.error("The block was attached via API but agent_state.memory doesn't reflect this!") 118 + 119 + # Show that the block still isn't in agent_state.memory 120 + logger.info("\nFinal blocks in agent_state.memory: " + ", ".join(agent_state.memory.blocks.keys())) 121 + logger.info("Note: The new block is NOT in agent_state.memory even though it's attached via API") 122 + 123 + 124 + def potential_solutions(): 125 + """Document potential solutions to this issue.""" 126 + print("\n" + "="*80) 127 + print("POTENTIAL SOLUTIONS:") 128 + print("="*80) 129 + print(""" 130 + 1. Refresh agent_state after tool execution: 131 + - After each tool call, reload agent_state from the database 132 + - This ensures agent_state.memory reflects API changes 133 + 134 + 2. Update agent_state.memory directly in attach_user_blocks: 135 + - Instead of only using API, also update agent_state.memory.blocks 136 + - This keeps the in-memory state synchronized 137 + 138 + 3. Use a different approach for dynamic blocks: 139 + - Have memory functions check the API for block existence 140 + - Or use a lazy-loading approach for blocks 141 + 142 + 4. Make agent_state.memory aware of API changes: 143 + - Implement a mechanism to sync agent_state with database changes 144 + - Could use events or callbacks when blocks are attached/detached 145 + 146 + The core issue is that agent_state is loaded once and becomes stale when 147 + tools make changes via the API during message processing. 148 + """) 149 + 150 + 151 + if __name__ == "__main__": 152 + print("Letta Dynamic Block Loading Issue - Minimal Reproduction\n") 153 + demonstrate_issue() 154 + potential_solutions()
+136
minimal_block_issue_simple.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Simplified minimal reproducible example for Letta dynamic block loading issue. 4 + 5 + This demonstrates the core issue: 6 + 1. A tool attaches a new block to an agent 7 + 2. Memory functions fail because agent_state doesn't reflect the new block 8 + """ 9 + 10 + import os 11 + import logging 12 + from dotenv import load_dotenv 13 + from letta_client import Letta 14 + 15 + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 16 + logger = logging.getLogger(__name__) 17 + 18 + load_dotenv() 19 + 20 + 21 + def main(): 22 + """Demonstrate the dynamic block loading issue.""" 23 + client = Letta(token=os.environ["LETTA_API_KEY"]) 24 + 25 + # Use an existing agent or create one using the utils 26 + agent_name = "test_block_issue" 27 + 28 + # First, let's see if we can create an agent using the API directly 29 + logger.info("Looking for or creating test agent...") 30 + 31 + agents = client.agents.list() 32 + agent = None 33 + for a in agents: 34 + if a.name == agent_name: 35 + agent = a 36 + logger.info(f"Found existing agent: {agent.name}") 37 + break 38 + 39 + if not agent: 40 + # Create using simple params that work 41 + agent = client.agents.create( 42 + name=agent_name 43 + ) 44 + logger.info(f"Created agent: {agent.name} (ID: {agent.id})") 45 + 46 + try: 47 + # Get initial blocks 48 + initial_blocks = client.agents.blocks.list(agent_id=str(agent.id)) 49 + initial_labels = {block.label for block in initial_blocks} 50 + logger.info(f"Initial blocks: {initial_labels}") 51 + 52 + # Step 1: Create and attach a new block 53 + test_handle = "testuser.bsky.social" 54 + block_label = f"user_{test_handle.replace('.', '_')}" 55 + 56 + # Check if block already attached 57 + if block_label in initial_labels: 58 + logger.info(f"Block {block_label} already attached, skipping creation") 59 + else: 60 + logger.info(f"Creating block: {block_label}") 61 + 62 + # Check if block exists 63 + existing_blocks = client.blocks.list(label=block_label) 64 + if existing_blocks: 65 + new_block = existing_blocks[0] 66 + logger.info(f"Using existing block with ID: {new_block.id}") 67 + else: 68 + # Create the block 69 + new_block = client.blocks.create( 70 + label=block_label, 71 + value=f"# User: {test_handle}\n\nInitial content.", 72 + limit=5000 73 + ) 74 + logger.info(f"Created block with ID: {new_block.id}") 75 + 76 + # Attach to agent 77 + logger.info("Attaching block to agent...") 78 + client.agents.blocks.attach( 79 + agent_id=str(agent.id), 80 + block_id=str(new_block.id) 81 + ) 82 + 83 + # Verify attachment via API 84 + blocks_after = client.agents.blocks.list(agent_id=str(agent.id)) 85 + labels_after = {block.label for block in blocks_after} 86 + logger.info(f"Blocks after attachment: {labels_after}") 87 + 88 + if block_label in labels_after: 89 + logger.info("✓ Block successfully attached via API") 90 + else: 91 + logger.error("✗ Block NOT found via API after attachment") 92 + 93 + # Step 2: Send a message asking the agent to use memory_insert on the new block 94 + logger.info(f"\nAsking agent to update the newly attached block...") 95 + 96 + from letta_client import MessageCreate 97 + 98 + response = client.agents.messages.create( 99 + agent_id=str(agent.id), 100 + 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.'")] 101 + ) 102 + 103 + # Check for errors in the response 104 + error_found = False 105 + for message in response.messages: 106 + if hasattr(message, 'text') and message.text: 107 + logger.info(f"Agent: {message.text}") 108 + 109 + # Look for tool returns with errors 110 + if hasattr(message, 'type') and message.type == 'tool_return': 111 + if hasattr(message, 'status') and message.status == 'error': 112 + error_found = True 113 + logger.error("ERROR REPRODUCED!") 114 + if hasattr(message, 'tool_return'): 115 + logger.error(f"Tool error: {message.tool_return}") 116 + if hasattr(message, 'stderr') and message.stderr: 117 + for err in message.stderr: 118 + logger.error(f"Stderr: {err}") 119 + 120 + if not error_found: 121 + logger.info("No error found - checking if operation succeeded...") 122 + 123 + # Get the block content to see if it was updated 124 + updated_blocks = client.blocks.list(label=block_label) 125 + if updated_blocks: 126 + logger.info(f"Block content:\n{updated_blocks[0].value}") 127 + 128 + finally: 129 + # Cleanup - always delete test agent 130 + if agent and agent.name == agent_name: 131 + logger.info(f"\nDeleting test agent {agent.name}") 132 + client.agents.delete(agent_id=str(agent.id)) 133 + 134 + 135 + if __name__ == "__main__": 136 + main()
-24
register_tools.py
··· 14 14 from tools.post import create_new_bluesky_post, PostArgs 15 15 from tools.feed import get_bluesky_feed, FeedArgs 16 16 from tools.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs 17 - from tools.defensive_memory import safe_memory_insert, safe_core_memory_replace 18 - from pydantic import BaseModel, Field 19 - 20 - class SafeMemoryInsertArgs(BaseModel): 21 - label: str = Field(..., description="Section of the memory to be edited, identified by its label") 22 - content: str = Field(..., description="Content to insert") 23 - insert_line: int = Field(-1, description="Line number after which to insert (-1 for end)") 24 - 25 - class SafeCoreMemoryReplaceArgs(BaseModel): 26 - label: str = Field(..., description="Section of the memory to be edited") 27 - old_content: str = Field(..., description="String to replace (must match exactly)") 28 - new_content: str = Field(..., description="New content to replace with") 29 17 30 18 load_dotenv() 31 19 logging.basicConfig(level=logging.INFO) ··· 64 52 "args_schema": DetachUserBlocksArgs, 65 53 "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.", 66 54 "tags": ["memory", "blocks", "user"] 67 - }, 68 - { 69 - "func": safe_memory_insert, 70 - "args_schema": SafeMemoryInsertArgs, 71 - "description": "SAFE: Insert text into a memory block. Handles missing blocks by fetching from API.", 72 - "tags": ["memory", "safe", "insert"] 73 - }, 74 - { 75 - "func": safe_core_memory_replace, 76 - "args_schema": SafeCoreMemoryReplaceArgs, 77 - "description": "SAFE: Replace content in a memory block. Handles missing blocks by fetching from API.", 78 - "tags": ["memory", "safe", "replace"] 79 55 }, 80 56 ] 81 57
-88
tools/defensive_memory.py
··· 1 - """Defensive memory operations that handle missing blocks gracefully.""" 2 - import os 3 - from typing import Optional 4 - from letta_client import Letta 5 - 6 - 7 - def safe_memory_insert(agent_state: "AgentState", label: str, content: str, insert_line: int = -1) -> str: 8 - """ 9 - Safe version of memory_insert that handles missing blocks by fetching them from API. 10 - 11 - This is a stopgap solution for the dynamic block loading issue where agent_state.memory 12 - doesn't reflect blocks that were attached via API during the same message processing cycle. 13 - """ 14 - try: 15 - # Try the normal memory_insert first 16 - from letta.functions.function_sets.base import memory_insert 17 - return memory_insert(agent_state, label, content, insert_line) 18 - 19 - except KeyError as e: 20 - if "does not exist" in str(e): 21 - print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...") 22 - # Try to fetch the block from the API and add it to agent_state.memory 23 - try: 24 - client = Letta(token=os.environ["LETTA_API_KEY"]) 25 - 26 - # Get all blocks attached to this agent 27 - api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 28 - 29 - # Find the block we're looking for 30 - target_block = None 31 - for block in api_blocks: 32 - if block.label == label: 33 - target_block = block 34 - break 35 - 36 - if target_block: 37 - # Add it to agent_state.memory 38 - agent_state.memory.set_block(target_block) 39 - print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory") 40 - 41 - # Now try the memory_insert again 42 - from letta.functions.function_sets.base import memory_insert 43 - return memory_insert(agent_state, label, content, insert_line) 44 - else: 45 - # Block truly doesn't exist 46 - raise Exception(f"Block {label} not found in API - it may not be attached to this agent") 47 - 48 - except Exception as api_error: 49 - raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}") 50 - else: 51 - raise e # Re-raise if it's a different KeyError 52 - 53 - 54 - def safe_core_memory_replace(agent_state: "AgentState", label: str, old_content: str, new_content: str) -> Optional[str]: 55 - """ 56 - Safe version of core_memory_replace that handles missing blocks. 57 - """ 58 - try: 59 - # Try the normal core_memory_replace first 60 - from letta.functions.function_sets.base import core_memory_replace 61 - return core_memory_replace(agent_state, label, old_content, new_content) 62 - 63 - except KeyError as e: 64 - if "does not exist" in str(e): 65 - print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...") 66 - try: 67 - client = Letta(token=os.environ["LETTA_API_KEY"]) 68 - api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 69 - 70 - target_block = None 71 - for block in api_blocks: 72 - if block.label == label: 73 - target_block = block 74 - break 75 - 76 - if target_block: 77 - agent_state.memory.set_block(target_block) 78 - print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory") 79 - 80 - from letta.functions.function_sets.base import core_memory_replace 81 - return core_memory_replace(agent_state, label, old_content, new_content) 82 - else: 83 - raise Exception(f"Block {label} not found in API - it may not be attached to this agent") 84 - 85 - except Exception as api_error: 86 - raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}") 87 - else: 88 - raise e
void_bot.log

This is a binary file and will not be displayed.