a digital person for bluesky

Fix synthesis acknowledgments in synthesis-only mode

Connect to Bluesky even in synthesis-only mode (unless --test is used) to enable posting synthesis acks when the agent calls annotate_ack tool.

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

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

Changed files
+206 -5
+206 -5
bsky.py
··· 108 108 # Skip git operations flag 109 109 SKIP_GIT = False 110 110 111 + # Synthesis message tracking 112 + last_synthesis_time = time.time() 113 + 111 114 def export_agent_state(client, agent, skip_git=False): 112 115 """Export agent state to agent_archive/ (timestamped) and agents/ (current).""" 113 116 try: ··· 314 317 unique_handles = list(all_handles) 315 318 316 319 logger.debug(f"Found {len(unique_handles)} unique handles in thread: {unique_handles}") 320 + 321 + # Check if any handles are in known_bots list 322 + from tools.bot_detection import check_known_bots, should_respond_to_bot_thread, CheckKnownBotsArgs 323 + import json 324 + 325 + try: 326 + # Check for known bots in thread 327 + bot_check_result = check_known_bots(unique_handles, void_agent) 328 + bot_check_data = json.loads(bot_check_result) 329 + 330 + if bot_check_data.get("bot_detected", False): 331 + detected_bots = bot_check_data.get("detected_bots", []) 332 + logger.info(f"Bot detected in thread: {detected_bots}") 333 + 334 + # Decide whether to respond (10% chance) 335 + if not should_respond_to_bot_thread(): 336 + logger.info(f"Skipping bot thread (90% skip rate). Detected bots: {detected_bots}") 337 + # Return False to keep in queue for potential later processing 338 + return False 339 + else: 340 + logger.info(f"Responding to bot thread (10% response rate). Detected bots: {detected_bots}") 341 + else: 342 + logger.debug("No known bots detected in thread") 343 + 344 + except Exception as bot_check_error: 345 + logger.warning(f"Error checking for bots: {bot_check_error}") 346 + # Continue processing if bot check fails 317 347 318 348 # Attach user blocks before agent call 319 349 attached_handles = [] ··· 1202 1232 logger.error(f"Error processing notifications: {e}") 1203 1233 1204 1234 1235 + def send_synthesis_message(client: Letta, agent_id: str, atproto_client=None) -> None: 1236 + """ 1237 + Send a synthesis message to the agent every 10 minutes. 1238 + This prompts the agent to synthesize its recent experiences. 1239 + 1240 + Args: 1241 + client: Letta client 1242 + agent_id: Agent ID to send synthesis to 1243 + atproto_client: Optional AT Protocol client for posting synthesis results 1244 + """ 1245 + try: 1246 + logger.info("🧠 Sending synthesis prompt to agent") 1247 + 1248 + # Send synthesis message with streaming to show tool use 1249 + message_stream = client.agents.messages.create_stream( 1250 + agent_id=agent_id, 1251 + messages=[{"role": "user", "content": "Synthesize."}], 1252 + stream_tokens=False, 1253 + max_steps=100 1254 + ) 1255 + 1256 + # Track synthesis content for potential posting 1257 + synthesis_posts = [] 1258 + ack_note = None 1259 + 1260 + # Process the streaming response 1261 + for chunk in message_stream: 1262 + if hasattr(chunk, 'message_type'): 1263 + if chunk.message_type == 'reasoning_message': 1264 + if SHOW_REASONING: 1265 + print("\n◆ Reasoning") 1266 + print(" ─────────") 1267 + for line in chunk.reasoning.split('\n'): 1268 + print(f" {line}") 1269 + elif chunk.message_type == 'tool_call_message': 1270 + tool_name = chunk.tool_call.name 1271 + try: 1272 + args = json.loads(chunk.tool_call.arguments) 1273 + if tool_name == 'archival_memory_search': 1274 + query = args.get('query', 'unknown') 1275 + log_with_panel(f"query: \"{query}\"", f"Tool call: {tool_name}", "blue") 1276 + elif tool_name == 'archival_memory_insert': 1277 + content = args.get('content', '') 1278 + log_with_panel(content[:200] + "..." if len(content) > 200 else content, f"Tool call: {tool_name}", "blue") 1279 + elif tool_name == 'update_block': 1280 + label = args.get('label', 'unknown') 1281 + value_preview = str(args.get('value', ''))[:100] + "..." if len(str(args.get('value', ''))) > 100 else str(args.get('value', '')) 1282 + log_with_panel(f"{label}: \"{value_preview}\"", f"Tool call: {tool_name}", "blue") 1283 + elif tool_name == 'annotate_ack': 1284 + note = args.get('note', '') 1285 + if note: 1286 + ack_note = note 1287 + log_with_panel(f"note: \"{note[:100]}...\"" if len(note) > 100 else f"note: \"{note}\"", f"Tool call: {tool_name}", "blue") 1288 + elif tool_name == 'add_post_to_bluesky_reply_thread': 1289 + text = args.get('text', '') 1290 + synthesis_posts.append(text) 1291 + log_with_panel(f"text: \"{text[:100]}...\"" if len(text) > 100 else f"text: \"{text}\"", f"Tool call: {tool_name}", "blue") 1292 + else: 1293 + args_str = ', '.join(f"{k}={v}" for k, v in args.items() if k != 'request_heartbeat') 1294 + if len(args_str) > 150: 1295 + args_str = args_str[:150] + "..." 1296 + log_with_panel(args_str, f"Tool call: {tool_name}", "blue") 1297 + except: 1298 + log_with_panel(chunk.tool_call.arguments[:150] + "...", f"Tool call: {tool_name}", "blue") 1299 + elif chunk.message_type == 'tool_return_message': 1300 + if chunk.status == 'success': 1301 + log_with_panel("Success", f"Tool result: {chunk.name} ✓", "green") 1302 + else: 1303 + log_with_panel("Error", f"Tool result: {chunk.name} ✗", "red") 1304 + elif chunk.message_type == 'assistant_message': 1305 + print("\n▶ Synthesis Response") 1306 + print(" ──────────────────") 1307 + for line in chunk.content.split('\n'): 1308 + print(f" {line}") 1309 + 1310 + if str(chunk) == 'done': 1311 + break 1312 + 1313 + logger.info("🧠 Synthesis message processed successfully") 1314 + 1315 + # Handle synthesis acknowledgments if we have an atproto client 1316 + if atproto_client and ack_note: 1317 + try: 1318 + result = bsky_utils.create_synthesis_ack(atproto_client, ack_note) 1319 + if result: 1320 + logger.info(f"✓ Created synthesis acknowledgment: {ack_note[:50]}...") 1321 + else: 1322 + logger.warning("Failed to create synthesis acknowledgment") 1323 + except Exception as e: 1324 + logger.error(f"Error creating synthesis acknowledgment: {e}") 1325 + 1326 + # Handle synthesis posts if any were generated 1327 + if atproto_client and synthesis_posts: 1328 + try: 1329 + for post_text in synthesis_posts: 1330 + cleaned_text = bsky_utils.remove_outside_quotes(post_text) 1331 + response = bsky_utils.send_post(atproto_client, cleaned_text) 1332 + if response: 1333 + logger.info(f"✓ Posted synthesis content: {cleaned_text[:50]}...") 1334 + else: 1335 + logger.warning(f"Failed to post synthesis content: {cleaned_text[:50]}...") 1336 + except Exception as e: 1337 + logger.error(f"Error posting synthesis content: {e}") 1338 + 1339 + except Exception as e: 1340 + logger.error(f"Error sending synthesis message: {e}") 1341 + 1342 + 1205 1343 def periodic_user_block_cleanup(client: Letta, agent_id: str) -> None: 1206 1344 """ 1207 1345 Detach all user blocks from the agent to prevent memory bloat. ··· 1252 1390 # --rich option removed as we now use simple text formatting 1253 1391 parser.add_argument('--reasoning', action='store_true', help='Display reasoning in panels and set reasoning log level to INFO') 1254 1392 parser.add_argument('--cleanup-interval', type=int, default=10, help='Run user block cleanup every N cycles (default: 10, 0 to disable)') 1393 + parser.add_argument('--synthesis-interval', type=int, default=600, help='Send synthesis message every N seconds (default: 600 = 10 minutes, 0 to disable)') 1394 + parser.add_argument('--synthesis-only', action='store_true', help='Run in synthesis-only mode (only send synthesis messages, no notification processing)') 1255 1395 args = parser.parse_args() 1256 1396 1257 1397 # Configure logging based on command line arguments ··· 1335 1475 logger.info(" - Queue files will not be deleted") 1336 1476 logger.info(" - Notifications will not be marked as seen") 1337 1477 print("\n") 1478 + 1479 + # Check for synthesis-only mode 1480 + SYNTHESIS_ONLY = args.synthesis_only 1481 + if SYNTHESIS_ONLY: 1482 + logger.info("=== RUNNING IN SYNTHESIS-ONLY MODE ===") 1483 + logger.info(" - Only synthesis messages will be sent") 1484 + logger.info(" - No notification processing") 1485 + logger.info(" - No Bluesky client needed") 1486 + print("\n") 1338 1487 """Main bot loop that continuously monitors for notifications.""" 1339 1488 global start_time 1340 1489 start_time = time.time() ··· 1361 1510 else: 1362 1511 logger.warning("Agent has no tools registered!") 1363 1512 1364 - # Initialize Bluesky client 1365 - atproto_client = bsky_utils.default_login() 1366 - logger.info("Connected to Bluesky") 1513 + # Initialize Bluesky client (needed for both notification processing and synthesis acks/posts) 1514 + if not SYNTHESIS_ONLY: 1515 + atproto_client = bsky_utils.default_login() 1516 + logger.info("Connected to Bluesky") 1517 + else: 1518 + # In synthesis-only mode, still connect for acks and posts (unless in test mode) 1519 + if not args.test: 1520 + atproto_client = bsky_utils.default_login() 1521 + logger.info("Connected to Bluesky (for synthesis acks/posts)") 1522 + else: 1523 + atproto_client = None 1524 + logger.info("Skipping Bluesky connection (test mode)") 1367 1525 1368 - # Main loop 1526 + # Configure intervals 1527 + CLEANUP_INTERVAL = args.cleanup_interval 1528 + SYNTHESIS_INTERVAL = args.synthesis_interval 1529 + 1530 + # Synthesis-only mode 1531 + if SYNTHESIS_ONLY: 1532 + if SYNTHESIS_INTERVAL <= 0: 1533 + logger.error("Synthesis-only mode requires --synthesis-interval > 0") 1534 + return 1535 + 1536 + logger.info(f"Starting synthesis-only mode, interval: {SYNTHESIS_INTERVAL} seconds ({SYNTHESIS_INTERVAL/60:.1f} minutes)") 1537 + 1538 + while True: 1539 + try: 1540 + # Send synthesis message immediately on first run 1541 + logger.info("🧠 Sending synthesis message") 1542 + send_synthesis_message(CLIENT, void_agent.id, atproto_client) 1543 + 1544 + # Wait for next interval 1545 + logger.info(f"Waiting {SYNTHESIS_INTERVAL} seconds until next synthesis...") 1546 + sleep(SYNTHESIS_INTERVAL) 1547 + 1548 + except KeyboardInterrupt: 1549 + logger.info("=== SYNTHESIS MODE STOPPED BY USER ===") 1550 + break 1551 + except Exception as e: 1552 + logger.error(f"Error in synthesis loop: {e}") 1553 + logger.info(f"Sleeping for {SYNTHESIS_INTERVAL} seconds due to error...") 1554 + sleep(SYNTHESIS_INTERVAL) 1555 + 1556 + # Normal mode with notification processing 1369 1557 logger.info(f"Starting notification monitoring, checking every {FETCH_NOTIFICATIONS_DELAY_SEC} seconds") 1370 1558 1371 1559 cycle_count = 0 1372 - CLEANUP_INTERVAL = args.cleanup_interval 1373 1560 1374 1561 if CLEANUP_INTERVAL > 0: 1375 1562 logger.info(f"User block cleanup enabled every {CLEANUP_INTERVAL} cycles") 1376 1563 else: 1377 1564 logger.info("User block cleanup disabled") 1378 1565 1566 + if SYNTHESIS_INTERVAL > 0: 1567 + logger.info(f"Synthesis messages enabled every {SYNTHESIS_INTERVAL} seconds ({SYNTHESIS_INTERVAL/60:.1f} minutes)") 1568 + else: 1569 + logger.info("Synthesis messages disabled") 1570 + 1379 1571 while True: 1380 1572 try: 1381 1573 cycle_count += 1 1382 1574 process_notifications(void_agent, atproto_client, TESTING_MODE) 1575 + 1576 + # Check if synthesis interval has passed 1577 + if SYNTHESIS_INTERVAL > 0: 1578 + current_time = time.time() 1579 + global last_synthesis_time 1580 + if current_time - last_synthesis_time >= SYNTHESIS_INTERVAL: 1581 + logger.info(f"⏰ {SYNTHESIS_INTERVAL/60:.1f} minutes have passed, triggering synthesis") 1582 + send_synthesis_message(CLIENT, void_agent.id, atproto_client) 1583 + last_synthesis_time = current_time 1383 1584 1384 1585 # Run periodic cleanup every N cycles 1385 1586 if CLEANUP_INTERVAL > 0 and cycle_count % CLEANUP_INTERVAL == 0: