a digital person for bluesky

Add flag_archival_memory_for_deletion tool for memory cleanup

Implements a new tool that allows the agent to flag archival memories for
deletion based on exact text matching. The tool works as a no-op that signals
the bot loop to perform deletions at the end of the turn.

Changes:
- Created tools/flag_memory_deletion.py with the tool definition
- Updated register_tools.py to include the new tool
- Modified bsky.py to handle flagged memory deletion:
- Tracks flagged memories during message processing
- Searches for exact text matches using passages.list()
- Deletes all matching passages using passages.delete()
- Executes after message processing but before reply handling
- Automatically skipped if halt_activity is called (due to immediate exit)

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

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

+88
+51
bsky.py
··· 661 661 reply_candidates = [] 662 662 tool_call_results = {} # Map tool_call_id to status 663 663 ack_note = None # Track any note from annotate_ack tool 664 + flagged_memories = [] # Track memories flagged for deletion 664 665 665 666 logger.debug(f"Processing {len(message_response.messages)} response messages...") 666 667 ··· 766 767 logger.debug(f"Found annotate_ack with note: {note[:50]}...") 767 768 except json.JSONDecodeError as e: 768 769 logger.error(f"Failed to parse annotate_ack arguments: {e}") 770 + 771 + # Collect flag_archival_memory_for_deletion tool calls 772 + elif message.tool_call.name == 'flag_archival_memory_for_deletion': 773 + try: 774 + args = json.loads(message.tool_call.arguments) 775 + memory_text = args.get('memory_text', '') 776 + if memory_text: 777 + flagged_memories.append(memory_text) 778 + logger.debug(f"Found memory flagged for deletion: {memory_text[:50]}...") 779 + except json.JSONDecodeError as e: 780 + logger.error(f"Failed to parse flag_archival_memory_for_deletion arguments: {e}") 769 781 770 782 # Collect add_post_to_bluesky_reply_thread tool calls - only if they were successful 771 783 elif message.tool_call.name == 'add_post_to_bluesky_reply_thread': ··· 787 799 logger.debug(f"Skipping failed add_post_to_bluesky_reply_thread tool call (status: error)") 788 800 else: 789 801 logger.warning(f"⚠️ Skipping add_post_to_bluesky_reply_thread tool call with unknown status: {tool_status}") 802 + 803 + # Handle archival memory deletion if any were flagged (only if no halt was received) 804 + if flagged_memories: 805 + logger.info(f"Processing {len(flagged_memories)} flagged memories for deletion") 806 + for memory_text in flagged_memories: 807 + try: 808 + # Search for passages with this exact text 809 + logger.debug(f"Searching for passages matching: {memory_text[:100]}...") 810 + passages = CLIENT.agents.passages.list( 811 + agent_id=void_agent.id, 812 + query=memory_text 813 + ) 814 + 815 + if not passages: 816 + logger.warning(f"No passages found matching flagged memory: {memory_text[:50]}...") 817 + continue 818 + 819 + # Delete all matching passages 820 + deleted_count = 0 821 + for passage in passages: 822 + # Check if the passage text exactly matches (to avoid partial matches) 823 + if hasattr(passage, 'text') and passage.text == memory_text: 824 + try: 825 + CLIENT.agents.passages.delete( 826 + agent_id=void_agent.id, 827 + passage_id=str(passage.id) 828 + ) 829 + deleted_count += 1 830 + logger.debug(f"Deleted passage {passage.id}") 831 + except Exception as delete_error: 832 + logger.error(f"Failed to delete passage {passage.id}: {delete_error}") 833 + 834 + if deleted_count > 0: 835 + logger.info(f"🗑️ Deleted {deleted_count} archival memory passage(s): {memory_text[:50]}...") 836 + else: 837 + logger.warning(f"No exact matches found for deletion: {memory_text[:50]}...") 838 + 839 + except Exception as e: 840 + logger.error(f"Error processing memory deletion: {e}") 790 841 791 842 # Check for conflicting tool calls 792 843 if reply_candidates and ignored_notification:
+7
register_tools.py
··· 20 20 from tools.whitewind import create_whitewind_blog_post, WhitewindPostArgs 21 21 from tools.ack import annotate_ack, AnnotateAckArgs 22 22 from tools.webpage import fetch_webpage, WebpageArgs 23 + from tools.flag_memory_deletion import flag_archival_memory_for_deletion, FlagArchivalMemoryForDeletionArgs 23 24 24 25 letta_config = get_letta_config() 25 26 logging.basicConfig(level=logging.INFO) ··· 114 115 "args_schema": WebpageArgs, 115 116 "description": "Fetch a webpage and convert it to markdown/text format using Jina AI reader", 116 117 "tags": ["web", "fetch", "webpage", "markdown", "jina"] 118 + }, 119 + { 120 + "func": flag_archival_memory_for_deletion, 121 + "args_schema": FlagArchivalMemoryForDeletionArgs, 122 + "description": "Flag an archival memory for deletion based on its exact text content", 123 + "tags": ["memory", "archival", "delete", "cleanup"] 117 124 }, 118 125 ] 119 126
+30
tools/flag_memory_deletion.py
··· 1 + """Flag archival memory for deletion tool.""" 2 + from pydantic import BaseModel, Field 3 + 4 + 5 + class FlagArchivalMemoryForDeletionArgs(BaseModel): 6 + memory_text: str = Field( 7 + ..., 8 + description="The exact text content of the archival memory to delete" 9 + ) 10 + 11 + 12 + def flag_archival_memory_for_deletion(memory_text: str) -> str: 13 + """ 14 + Flag an archival memory for deletion based on its exact text content. 15 + 16 + This is a "dummy" tool that doesn't directly delete memories but signals to the system 17 + that the specified memory should be deleted at the end of the turn (if no halt_activity 18 + has been received). 19 + 20 + The system will search for all archival memories with this exact text and delete them. 21 + 22 + Args: 23 + memory_text: The exact text content of the archival memory to delete 24 + 25 + Returns: 26 + Confirmation message 27 + """ 28 + # This is a dummy tool - it just returns a confirmation 29 + # The actual deletion will be handled by the bot loop after the agent's turn completes 30 + return f"Memory flagged for deletion. Will be removed at the end of this turn if no halt is received."