a digital person for bluesky
1#!/usr/bin/env python3
2"""
3Minimal reproducible example for Letta dynamic block loading issue.
4
5This demonstrates the core problem:
61. A tool dynamically attaches a new block to an agent via the API
72. 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
93. The error occurs because agent_state.memory.get_block() throws KeyError
10
11The issue appears to be that agent_state is loaded once at the beginning of processing
12a message and isn't refreshed after tools make changes via the API.
13"""
14
15import os
16import logging
17from typing import Optional
18from dotenv import load_dotenv
19
20logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21logger = logging.getLogger(__name__)
22
23load_dotenv()
24
25
26class 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
34class 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
56def 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
78def 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
96def 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
124def potential_solutions():
125 """Document potential solutions to this issue."""
126 print("\n" + "="*80)
127 print("POTENTIAL SOLUTIONS:")
128 print("="*80)
129 print("""
1301. 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
1342. 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
1383. 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
1424. 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
146The core issue is that agent_state is loaded once and becomes stale when
147tools make changes via the API during message processing.
148""")
149
150
151if __name__ == "__main__":
152 print("Letta Dynamic Block Loading Issue - Minimal Reproduction\n")
153 demonstrate_issue()
154 potential_solutions()