a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

run agent loop for all notification types

likes, reposts, and follows now go through the full agent pipeline.
phi decides what's worth responding to, not the handler.

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

zzstoatzz db2471ea f1ff98f8

+74 -7
+5
src/bot/agent.py
··· 22 22 OPERATIONAL_INSTRUCTIONS = """ 23 23 indicate your response action via the structured output — do not use atproto tools to post, like, or repost directly. 24 24 when sharing URLs, verify them with check_urls first and always include https://. 25 + 26 + you receive all notification types — mentions, replies, quotes, likes, reposts, and follows. 27 + for mentions, replies, and quotes: someone is talking to you or about you. respond if you have something to say. 28 + for likes and reposts: someone engaged with your content. usually just note it and ignore, but if something about the person or context is genuinely interesting, you can respond. 29 + for follows: someone followed you. same principle — note it, ignore unless curious. 25 30 """.strip() 26 31 27 32
+69 -7
src/bot/services/message_handler.py
··· 20 20 self.agent = PhiAgent() 21 21 22 22 async def handle_notification(self, notification): 23 - """Process any notification type.""" 23 + """Process any notification through the agent.""" 24 24 reason = notification.reason 25 25 author_handle = notification.author.handle 26 26 ··· 28 28 if reason in ("mention", "reply", "quote"): 29 29 await self._handle_post(notification) 30 30 elif reason in ("like", "repost"): 31 - # someone engaged with phi's content — log for awareness 32 - logger.info(f"{reason} from @{author_handle}") 33 - bot_status.record_mention() 31 + await self._handle_engagement(notification) 34 32 elif reason == "follow": 35 - logger.info(f"followed by @{author_handle}") 36 - bot_status.record_mention() 33 + await self._handle_follow(notification) 37 34 else: 38 35 logger.debug(f"notification type '{reason}' from @{author_handle}") 39 36 except Exception as e: 40 37 logger.exception(f"notification handling error: {e}") 41 38 bot_status.record_error() 42 39 40 + async def _handle_engagement(self, notification): 41 + """Process a like or repost — someone engaged with phi's content.""" 42 + reason = notification.reason 43 + author_handle = notification.author.handle 44 + post_uri = notification.uri 45 + 46 + # Fetch phi's post that was liked/reposted 47 + posts = await self.client.get_posts([post_uri]) 48 + if not posts.posts: 49 + logger.warning(f"could not find post {post_uri}") 50 + return 51 + 52 + post = posts.posts[0] 53 + post_text = post.record.text if hasattr(post.record, "text") else "" 54 + 55 + bot_status.record_mention() 56 + 57 + mention_text = f"[notification: @{author_handle} {reason}d your post]\nyour post: {post_text}" 58 + 59 + response = await self.agent.process_mention( 60 + mention_text=mention_text, 61 + author_handle=author_handle, 62 + thread_context="", 63 + ) 64 + 65 + if response.action == "ignore": 66 + logger.info(f"ignoring {reason} from @{author_handle}: {response.reason}") 67 + elif response.action == "reply" and response.text: 68 + # reply to phi's own post as a follow-up 69 + parent_ref = models.ComAtprotoRepoStrongRef.Main(uri=post_uri, cid=post.cid) 70 + if hasattr(post.record, "reply") and post.record.reply: 71 + root_ref = post.record.reply.root 72 + else: 73 + root_ref = parent_ref 74 + reply_ref = models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref) 75 + await self.client.create_post(response.text, reply_to=reply_ref) 76 + bot_status.record_response() 77 + logger.info(f"replied on {reason} from @{author_handle}: {response.text[:80]}") 78 + else: 79 + logger.info(f"{response.action} on {reason} from @{author_handle}") 80 + bot_status.record_response() 81 + 82 + async def _handle_follow(self, notification): 83 + """Process a follow notification.""" 84 + author_handle = notification.author.handle 85 + 86 + bot_status.record_mention() 87 + 88 + mention_text = f"[notification: @{author_handle} followed you]" 89 + 90 + response = await self.agent.process_mention( 91 + mention_text=mention_text, 92 + author_handle=author_handle, 93 + thread_context="", 94 + ) 95 + 96 + if response.action == "ignore": 97 + logger.info(f"ignoring follow from @{author_handle}: {response.reason}") 98 + elif response.action == "reply" and response.text: 99 + # post as a top-level post since there's no thread to reply to 100 + await self.client.create_post(response.text) 101 + bot_status.record_response() 102 + logger.info(f"posted on follow from @{author_handle}: {response.text[:80]}") 103 + else: 104 + logger.info(f"{response.action} on follow from @{author_handle}") 105 + 43 106 async def _handle_post(self, notification): 44 107 """Process a mention, reply, or quote notification.""" 45 108 post_uri = notification.uri ··· 115 178 return 116 179 117 180 elif response.action == "reply" and response.text: 118 - # Post reply 119 181 reply_ref = models.AppBskyFeedPost.ReplyRef( 120 182 parent=parent_ref, root=root_ref 121 183 )