···142142 logger.error(f"Error fetching thread: {e}")
143143 raise
144144145145+ print(thread)
146146+145147 # Get thread context as YAML string
146148 logger.info("Converting thread to YAML string")
147149 try:
+14-8
bsky_utils.py
···234234 facets = []
235235 text_bytes = text.encode("UTF-8")
236236237237- # Parse mentions
238238- mention_regex = rb"[$|\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)"
237237+ # Parse mentions - fixed to handle @ at start of text
238238+ mention_regex = rb"(?:^|[$|\W])(@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)"
239239240240 for m in re.finditer(mention_regex, text_bytes):
241241 handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix
242242+ # Adjust byte positions to account for the optional prefix
243243+ mention_start = m.start(1)
244244+ mention_end = m.end(1)
242245 try:
243246 # Resolve handle to DID using the API
244247 resolve_resp = client.app.bsky.actor.get_profile({'actor': handle})
···246249 facets.append(
247250 models.AppBskyRichtextFacet.Main(
248251 index=models.AppBskyRichtextFacet.ByteSlice(
249249- byteStart=m.start(1),
250250- byteEnd=m.end(1)
252252+ byteStart=mention_start,
253253+ byteEnd=mention_end
251254 ),
252255 features=[models.AppBskyRichtextFacet.Mention(did=resolve_resp.did)]
253256 )
···256259 logger.debug(f"Failed to resolve handle {handle}: {e}")
257260 continue
258261259259- # Parse URLs
260260- url_regex = rb"[$|\W](https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)"
262262+ # Parse URLs - fixed to handle URLs at start of text
263263+ url_regex = rb"(?:^|[$|\W])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)"
261264262265 for m in re.finditer(url_regex, text_bytes):
263266 url = m.group(1).decode("UTF-8")
267267+ # Adjust byte positions to account for the optional prefix
268268+ url_start = m.start(1)
269269+ url_end = m.end(1)
264270 facets.append(
265271 models.AppBskyRichtextFacet.Main(
266272 index=models.AppBskyRichtextFacet.ByteSlice(
267267- byteStart=m.start(1),
268268- byteEnd=m.end(1)
273273+ byteStart=url_start,
274274+ byteEnd=url_end
269275 ),
270276 features=[models.AppBskyRichtextFacet.Link(uri=url)]
271277 )
+24-6
register_tools.py
···1414from tools.post import create_new_bluesky_post, PostArgs
1515from tools.feed import get_bluesky_feed, FeedArgs
1616from tools.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs
1717+from tools.defensive_memory import safe_memory_insert, safe_core_memory_replace
1818+from pydantic import BaseModel, Field
1919+2020+class SafeMemoryInsertArgs(BaseModel):
2121+ label: str = Field(..., description="Section of the memory to be edited, identified by its label")
2222+ content: str = Field(..., description="Content to insert")
2323+ insert_line: int = Field(-1, description="Line number after which to insert (-1 for end)")
2424+2525+class SafeCoreMemoryReplaceArgs(BaseModel):
2626+ label: str = Field(..., description="Section of the memory to be edited")
2727+ old_content: str = Field(..., description="String to replace (must match exactly)")
2828+ new_content: str = Field(..., description="New content to replace with")
17291830load_dotenv()
1931logging.basicConfig(level=logging.INFO)
···5365 "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.",
5466 "tags": ["memory", "blocks", "user"]
5567 },
5656- # {
5757- # "func": update_user_blocks,
5858- # "args_schema": UpdateUserBlockArgs,
5959- # "description": "Update the content of user-specific memory blocks",
6060- # "tags": ["memory", "blocks", "user"]
6161- # },
6868+ {
6969+ "func": safe_memory_insert,
7070+ "args_schema": SafeMemoryInsertArgs,
7171+ "description": "SAFE: Insert text into a memory block. Handles missing blocks by fetching from API.",
7272+ "tags": ["memory", "safe", "insert"]
7373+ },
7474+ {
7575+ "func": safe_core_memory_replace,
7676+ "args_schema": SafeCoreMemoryReplaceArgs,
7777+ "description": "SAFE: Replace content in a memory block. Handles missing blocks by fetching from API.",
7878+ "tags": ["memory", "safe", "replace"]
7979+ },
6280]
63816482
+8
tools/blocks.py
···6969 agent_id=str(agent_state.id),
7070 block_id=str(block.id)
7171 )
7272+7373+ # STOPGAP: Also update agent_state.memory to sync in-memory state
7474+ try:
7575+ agent_state.memory.set_block(block)
7676+ print(f"[SYNC] Successfully synced block {block_label} to agent_state.memory")
7777+ except Exception as sync_error:
7878+ print(f"[SYNC] Warning: Failed to sync block to agent_state.memory: {sync_error}")
7979+7280 results.append(f"✓ {handle}: Block attached")
7381 logger.info(f"Successfully attached block {block_label} to agent")
7482
+88
tools/defensive_memory.py
···11+"""Defensive memory operations that handle missing blocks gracefully."""
22+import os
33+from typing import Optional
44+from letta_client import Letta
55+66+77+def safe_memory_insert(agent_state: "AgentState", label: str, content: str, insert_line: int = -1) -> str:
88+ """
99+ Safe version of memory_insert that handles missing blocks by fetching them from API.
1010+1111+ This is a stopgap solution for the dynamic block loading issue where agent_state.memory
1212+ doesn't reflect blocks that were attached via API during the same message processing cycle.
1313+ """
1414+ try:
1515+ # Try the normal memory_insert first
1616+ from letta.functions.function_sets.base import memory_insert
1717+ return memory_insert(agent_state, label, content, insert_line)
1818+1919+ except KeyError as e:
2020+ if "does not exist" in str(e):
2121+ print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...")
2222+ # Try to fetch the block from the API and add it to agent_state.memory
2323+ try:
2424+ client = Letta(token=os.environ["LETTA_API_KEY"])
2525+2626+ # Get all blocks attached to this agent
2727+ api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id))
2828+2929+ # Find the block we're looking for
3030+ target_block = None
3131+ for block in api_blocks:
3232+ if block.label == label:
3333+ target_block = block
3434+ break
3535+3636+ if target_block:
3737+ # Add it to agent_state.memory
3838+ agent_state.memory.set_block(target_block)
3939+ print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory")
4040+4141+ # Now try the memory_insert again
4242+ from letta.functions.function_sets.base import memory_insert
4343+ return memory_insert(agent_state, label, content, insert_line)
4444+ else:
4545+ # Block truly doesn't exist
4646+ raise Exception(f"Block {label} not found in API - it may not be attached to this agent")
4747+4848+ except Exception as api_error:
4949+ raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}")
5050+ else:
5151+ raise e # Re-raise if it's a different KeyError
5252+5353+5454+def safe_core_memory_replace(agent_state: "AgentState", label: str, old_content: str, new_content: str) -> Optional[str]:
5555+ """
5656+ Safe version of core_memory_replace that handles missing blocks.
5757+ """
5858+ try:
5959+ # Try the normal core_memory_replace first
6060+ from letta.functions.function_sets.base import core_memory_replace
6161+ return core_memory_replace(agent_state, label, old_content, new_content)
6262+6363+ except KeyError as e:
6464+ if "does not exist" in str(e):
6565+ print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...")
6666+ try:
6767+ client = Letta(token=os.environ["LETTA_API_KEY"])
6868+ api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id))
6969+7070+ target_block = None
7171+ for block in api_blocks:
7272+ if block.label == label:
7373+ target_block = block
7474+ break
7575+7676+ if target_block:
7777+ agent_state.memory.set_block(target_block)
7878+ print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory")
7979+8080+ from letta.functions.function_sets.base import core_memory_replace
8181+ return core_memory_replace(agent_state, label, old_content, new_content)
8282+ else:
8383+ raise Exception(f"Block {label} not found in API - it may not be attached to this agent")
8484+8585+ except Exception as api_error:
8686+ raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}")
8787+ else:
8888+ raise e
+14-8
tools/post.py
···9999 # Add facets for mentions and URLs
100100 facets = []
101101102102- # Parse mentions
103103- mention_regex = rb"[$|\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)"
102102+ # Parse mentions - fixed to handle @ at start of text
103103+ mention_regex = rb"(?:^|[$|\W])(@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)"
104104 text_bytes = post_text.encode("UTF-8")
105105106106 for m in re.finditer(mention_regex, text_bytes):
107107 handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix
108108+ # Adjust byte positions to account for the optional prefix
109109+ mention_start = m.start(1)
110110+ mention_end = m.end(1)
108111 try:
109112 resolve_resp = requests.get(
110113 f"{pds_host}/xrpc/com.atproto.identity.resolveHandle",
···115118 did = resolve_resp.json()["did"]
116119 facets.append({
117120 "index": {
118118- "byteStart": m.start(1),
119119- "byteEnd": m.end(1),
121121+ "byteStart": mention_start,
122122+ "byteEnd": mention_end,
120123 },
121124 "features": [{"$type": "app.bsky.richtext.facet#mention", "did": did}],
122125 })
123126 except:
124127 continue
125128126126- # Parse URLs
127127- url_regex = rb"[$|\W](https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)"
129129+ # Parse URLs - fixed to handle URLs at start of text
130130+ url_regex = rb"(?:^|[$|\W])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)"
128131129132 for m in re.finditer(url_regex, text_bytes):
130133 url = m.group(1).decode("UTF-8")
134134+ # Adjust byte positions to account for the optional prefix
135135+ url_start = m.start(1)
136136+ url_end = m.end(1)
131137 facets.append({
132138 "index": {
133133- "byteStart": m.start(1),
134134- "byteEnd": m.end(1),
139139+ "byteStart": url_start,
140140+ "byteEnd": url_end,
135141 },
136142 "features": [{"$type": "app.bsky.richtext.facet#link", "uri": url}],
137143 })