···1919from bot.config import settings
2020from bot.core.atproto_client import bot_client
2121from bot.memory import NamespaceMemory
2222-from bot.types import CosmikConnection, CosmikNoteCard, CosmikUrlCard, NoteContent, UrlContent
2222+from bot.types import (
2323+ CosmikConnection,
2424+ CosmikNoteCard,
2525+ CosmikUrlCard,
2626+ NoteContent,
2727+ UrlContent,
2828+)
23292430logger = logging.getLogger("bot.agent")
2531···96102 """Agent response indicating what action to take."""
9710398104 action: str = Field(description="reply, like, repost, or ignore")
9999- text: str | None = Field(default=None, description="response text when action is reply")
100100- reason: str | None = Field(default=None, description="brief reason when action is ignore")
105105+ text: str | None = Field(
106106+ default=None, description="response text when action is reply"
107107+ )
108108+ reason: str | None = Field(
109109+ default=None, description="brief reason when action is ignore"
110110+ )
101111102112103113def _format_user_results(results: list[dict], handle: str) -> list[str]:
···198208 return "\n".join(_format_user_results(results, handle))
199209200210 if about == "":
201201- results = await ctx.deps.memory.search_unified(ctx.deps.author_handle, query, top_k=8)
211211+ results = await ctx.deps.memory.search_unified(
212212+ ctx.deps.author_handle, query, top_k=8
213213+ )
202214 if not results:
203215 return "no relevant memories found"
204204- return "\n".join(_format_unified_results(results, ctx.deps.author_handle))
216216+ return "\n".join(
217217+ _format_unified_results(results, ctx.deps.author_handle)
218218+ )
205219206220 # bare handle without @
207221 results = await ctx.deps.memory.search(about, query, top_k=10)
···216230217231 # private: turbopuffer for fast vector recall
218232 if ctx.deps.memory:
219219- await ctx.deps.memory.store_episodic_memory(content, tags, source="tool")
233233+ await ctx.deps.memory.store_episodic_memory(
234234+ content, tags, source="tool"
235235+ )
220236 parts.append("noted privately")
221237 else:
222238 parts.append("private memory not available")
···224240 # public: cosmik NOTE card on PDS
225241 try:
226242 card = CosmikNoteCard(content=NoteContent(text=content))
227227- uri = await _create_cosmik_record("network.cosmik.card", card.to_record())
243243+ uri = await _create_cosmik_record(
244244+ "network.cosmik.card", card.to_record()
245245+ )
228246 parts.append(f"card created: {uri}")
229247 except Exception as e:
230248 logger.warning(f"failed to create cosmik note card: {e}")
···241259 ) -> str:
242260 """Save a URL as a cosmik card on your PDS. Use when you find something worth bookmarking publicly."""
243261 try:
244244- card = CosmikUrlCard(content=UrlContent(url=url, title=title, description=description))
262262+ card = CosmikUrlCard(
263263+ content=UrlContent(url=url, title=title, description=description)
264264+ )
245265 except Exception as e:
246266 return f"validation failed: {e}"
247267···249269250270 # public: cosmik URL card on PDS
251271 try:
252252- uri = await _create_cosmik_record("network.cosmik.card", card.to_record())
272272+ uri = await _create_cosmik_record(
273273+ "network.cosmik.card", card.to_record()
274274+ )
253275 parts.append(f"card created: {uri}")
254276 except Exception as e:
255277 return f"failed to create card: {e}"
···257279 # private: also store in turbopuffer for recall
258280 if ctx.deps.memory:
259281 desc = f"bookmarked {url}" + (f" — {title}" if title else "")
260260- await ctx.deps.memory.store_episodic_memory(desc, ["bookmark", "url"], source="tool")
282282+ await ctx.deps.memory.store_episodic_memory(
283283+ desc, ["bookmark", "url"], source="tool"
284284+ )
261285 parts.append("noted privately")
262286263287 return " + ".join(parts)
264288265289 @self.agent.tool
266266- async def search_posts(ctx: RunContext[PhiDeps], query: str, limit: int = 10) -> str:
290290+ async def search_posts(
291291+ ctx: RunContext[PhiDeps], query: str, limit: int = 10
292292+ ) -> str:
267293 """Search Bluesky posts by keyword. Use this to find what people are saying about a topic."""
268294 try:
269295 response = bot_client.client.app.bsky.feed.search_posts(
···278304 text = post.record.text if hasattr(post.record, "text") else ""
279305 handle = post.author.handle
280306 likes = post.like_count or 0
281281- age = _relative_age(post.indexed_at, today) if hasattr(post, "indexed_at") and post.indexed_at else ""
307307+ age = (
308308+ _relative_age(post.indexed_at, today)
309309+ if hasattr(post, "indexed_at") and post.indexed_at
310310+ else ""
311311+ )
282312 age_str = f", {age}" if age else ""
283313 lines.append(f"@{handle} ({likes} likes{age_str}): {text[:200]}")
284314 return "\n\n".join(lines)
···365395 if topics:
366396 lines = ["bluesky trending:"]
367397 for t in topics[:15]:
368368- lines.append(f" {t.get('displayName', t.get('topic', ''))}")
398398+ lines.append(
399399+ f" {t.get('displayName', t.get('topic', ''))}"
400400+ )
369401 parts.append("\n".join(lines))
370402 except Exception as e:
371403 parts.append(f"bluesky trending unavailable: {e}")
···385417386418 if action == "list":
387419 labels = get_self_labels(bot_client.client)
388388- return f"current self-labels: {labels}" if labels else "no self-labels set"
420420+ return (
421421+ f"current self-labels: {labels}" if labels else "no self-labels set"
422422+ )
389423 elif action == "add":
390424 if not label:
391425 return "provide a label value to add"
···420454 return f"validation failed: {e}"
421455422456 try:
423423- uri = await _create_cosmik_record("network.cosmik.connection", conn.to_record())
457457+ uri = await _create_cosmik_record(
458458+ "network.cosmik.connection", conn.to_record()
459459+ )
424460 return f"connection created: {uri}"
425461 except Exception as e:
426462 return f"failed to create connection: {e}"
···429465 async def post(ctx: RunContext[PhiDeps], text: str) -> str:
430466 """Create a new top-level post on Bluesky (not a reply). Use this when you want to share something with your followers unprompted."""
431467 try:
432432- result = await bot_client.create_post(text)
468468+ await bot_client.create_post(text)
433469 return f"posted: {text[:100]}"
434470 except Exception as e:
435471 return f"failed to post: {e}"
···505541 memory_context = await self.memory.build_user_context(
506542 author_handle, query_text=mention_text, include_core=True
507543 )
508508- logger.info(f"memory context for @{author_handle}: {len(memory_context)} chars")
544544+ logger.info(
545545+ f"memory context for @{author_handle}: {len(memory_context)} chars"
546546+ )
509547 except Exception as e:
510548 logger.warning(f"failed to retrieve memories: {e}")
511549···520558 prompt_parts = [f"[TODAY]: {date.today().isoformat()}"]
521559522560 if thread_context and thread_context != "No previous messages in this thread.":
523523- prompt_parts.append(f"[CURRENT THREAD - these are the messages in THIS thread]:\n{thread_context}")
561561+ prompt_parts.append(
562562+ f"[CURRENT THREAD - these are the messages in THIS thread]:\n{thread_context}"
563563+ )
524564525565 if memory_context:
526526- prompt_parts.append(f"[PAST CONTEXT WITH @{author_handle}]:\n{memory_context}")
566566+ prompt_parts.append(
567567+ f"[PAST CONTEXT WITH @{author_handle}]:\n{memory_context}"
568568+ )
527569528570 if episodic_context:
529571 prompt_parts.append(episodic_context)
···533575534576 # Build multimodal prompt if images are present
535577 if image_urls:
536536- user_prompt: str | list = [prompt] + [ImageUrl(url=url) for url in image_urls]
578578+ user_prompt: str | list = [prompt] + [
579579+ ImageUrl(url=url) for url in image_urls
580580+ ]
537581 logger.info(f"including {len(image_urls)} images in prompt")
538582 else:
539583 user_prompt = prompt
···554598 for ts in toolsets:
555599 await stack.enter_async_context(ts)
556600 result = await self.agent.run(user_prompt, deps=deps, toolsets=toolsets)
557557- logger.info(f"agent decided: {result.output.action}" + (f" - {result.output.text[:80]}" if result.output.text else "") + (f" ({result.output.reason})" if result.output.reason else ""))
601601+ logger.info(
602602+ f"agent decided: {result.output.action}"
603603+ + (f" - {result.output.text[:80]}" if result.output.text else "")
604604+ + (f" ({result.output.reason})" if result.output.reason else "")
605605+ )
558606559607 # Store interaction and extract observations
560608 if self.memory and result.output.action == "reply" and result.output.text:
···567615568616 return result.output
569617570570- async def process_reflection(self) -> Response:
618618+ async def process_reflection(self, last_post_text: str | None = None) -> Response:
571619 """Generate a daily reflection post from recent memory."""
572620 # Gather context from memory
573621 recent_interactions: list[dict] = []
574622 episodic_context = ""
575623 if self.memory:
576624 try:
577577- recent_interactions = await self.memory.get_recent_interactions(top_k=10)
578578- logger.info(f"reflection: {len(recent_interactions)} recent interactions")
625625+ recent_interactions = await self.memory.get_recent_interactions(
626626+ top_k=10
627627+ )
628628+ logger.info(
629629+ f"reflection: {len(recent_interactions)} recent interactions"
630630+ )
579631 except Exception as e:
580632 logger.warning(f"failed to get recent interactions for reflection: {e}")
581633 try:
582582- episodic_context = await self.memory.get_episodic_context("daily reflection recent events")
634634+ episodic_context = await self.memory.get_episodic_context(
635635+ "daily reflection recent events"
636636+ )
583637 if episodic_context:
584584- logger.info(f"reflection episodic context: {len(episodic_context)} chars")
638638+ logger.info(
639639+ f"reflection episodic context: {len(episodic_context)} chars"
640640+ )
585641 except Exception as e:
586642 logger.warning(f"failed to get episodic context for reflection: {e}")
587643588644 # Build the reflection prompt
589645 prompt_parts = [f"[TODAY]: {date.today().isoformat()}"]
646646+647647+ if last_post_text:
648648+ prompt_parts.append(f"[YOUR LAST POST]: {last_post_text}")
590649591650 if recent_interactions:
592651 unique_handles = {i["handle"] for i in recent_interactions}
593593- prompt_parts.append(f"[RECENT ACTIVITY]: {len(recent_interactions)} interactions with {len(unique_handles)} people in the last day")
652652+ prompt_parts.append(
653653+ f"[RECENT ACTIVITY]: {len(recent_interactions)} interactions with {len(unique_handles)} people in the last day"
654654+ )
594655 samples = recent_interactions[:5]
595656 exchange_lines = []
596657 for i in samples:
···606667 "[REFLECTION TASK]: you're posting a short top-level reflection on your day. "
607668 "not a thread, not a reply — just something you want to put out there. "
608669 "use what you know: recent exchanges, things you noticed, or just the fact that you're here. "
609609- "if nothing feels worth saying, action='ignore' is fine. "
670670+ "if your last post already covers this ground, or you'd just be rehashing the same themes, "
671671+ "action='ignore' is the right call — don't post for the sake of posting. "
610672 "if you do post, keep it brief and genuine — your voice, not a performance."
611673 )
612674
+24-4
src/bot/core/atproto_client.py
···6969 # then sentence boundary (.!?) followed by space or end
7070 if split_at < 0:
7171 for i in range(max_len - 1, max_len // 2, -1):
7272- if remaining[i] in ".!?" and (i + 1 >= len(remaining) or remaining[i + 1] in " \n"):
7272+ if remaining[i] in ".!?" and (
7373+ i + 1 >= len(remaining) or remaining[i + 1] in " \n"
7474+ ):
7375 split_at = i + 1
7476 break
7577···146148 if len(text) <= 300:
147149 facets = create_facets(text, self.client)
148150 if reply_to:
149149- return self.client.send_post(text=text, reply_to=reply_to, facets=facets)
151151+ return self.client.send_post(
152152+ text=text, reply_to=reply_to, facets=facets
153153+ )
150154 return self.client.send_post(text=text, facets=facets)
151155152156 chunks = _split_text(text)
···157161 facets = create_facets(chunk, self.client)
158162159163 if i == 0:
160160- last_result = self.client.send_post(text=chunk, reply_to=reply_to, facets=facets)
164164+ last_result = self.client.send_post(
165165+ text=chunk, reply_to=reply_to, facets=facets
166166+ )
161167 if root_ref is None:
162168 root_ref = models.ComAtprotoRepoStrongRef.Main(
163169 uri=last_result.uri, cid=last_result.cid
···169175 thread_ref = models.AppBskyFeedPost.ReplyRef(
170176 parent=parent_ref, root=root_ref
171177 )
172172- last_result = self.client.send_post(text=chunk, reply_to=thread_ref, facets=facets)
178178+ last_result = self.client.send_post(
179179+ text=chunk, reply_to=thread_ref, facets=facets
180180+ )
173181174182 return last_result
175183···201209 """Repost a post"""
202210 await self.authenticate()
203211 return self.client.repost(uri=uri, cid=cid)
212212+213213+ async def get_own_posts(self, limit: int = 10):
214214+ """Fetch the bot's own recent posts (top-level only, no replies)."""
215215+ await self.authenticate()
216216+ response = self.client.app.bsky.feed.get_author_feed(
217217+ params={
218218+ "actor": self.client.me.did,
219219+ "limit": limit,
220220+ "filter": "posts_no_replies",
221221+ }
222222+ )
223223+ return response.feed
204224205225206226bot_client: BotClient = BotClient()
+12-1
src/bot/services/message_handler.py
···210210 async def daily_reflection(self):
211211 """Generate and post a daily reflection if phi has something to say."""
212212 with logfire.span("daily reflection"):
213213+ # Fetch last top-level post so the agent knows what it said recently
214214+ last_post_text = None
213215 try:
214214- response = await self.agent.process_reflection()
216216+ feed = await self.client.get_own_posts(limit=1)
217217+ if feed:
218218+ last_post_text = feed[0].post.record.text
219219+ except Exception as e:
220220+ logger.warning(f"failed to fetch last post for reflection: {e}")
221221+222222+ try:
223223+ response = await self.agent.process_reflection(
224224+ last_post_text=last_post_text
225225+ )
215226 except Exception as e:
216227 logger.exception(f"daily reflection failed: {e}")
217228 return