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.

fix check_urls loop — track checked URLs, short-circuit after 10

the actual problem: phi was brute-force guessing blog post slugs (100+
different URLs across 40 check_urls calls) when given a truncated URL
it couldn't resolve. it's not retrying the same URL — it's hallucinating
new slugs and checking each one.

fix: check_urls tracks URLs checked per agent run. after 10 unique URLs,
it returns a message telling the model to stop guessing and respond with
what it found. reverts the request_limit=15 cap — that was a blunt
instrument that doesn't address the root cause.

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

+19 -14
+19 -14
src/bot/agent.py
··· 16 16 from pydantic import BaseModel, Field 17 17 from pydantic_ai import Agent, ImageUrl, RunContext 18 18 from pydantic_ai.mcp import MCPServerStreamableHTTP 19 - from pydantic_ai.usage import UsageLimits 20 19 21 20 from bot.config import settings 22 21 from bot.core.atproto_client import bot_client ··· 639 638 except Exception as e: 640 639 return f"failed to post: {e}" 641 640 641 + self._checked_urls: set[str] = set() 642 + 642 643 @self.agent.tool 643 644 async def check_urls(ctx: RunContext[PhiDeps], urls: list[str]) -> str: 644 - """Check whether URLs are reachable (HEAD request only — cannot read page content). Never call this more than once per URL. If a URL returns an error, accept that result and move on.""" 645 + """Check whether URLs are reachable (HEAD request only — cannot read page content). If a URL 404s, do not guess alternative slugs — tell the user you can't find it.""" 646 + 647 + # if we've already checked many URLs, the model is probably guessing slugs 648 + new_urls = [u for u in urls if u not in self._checked_urls] 649 + if not new_urls and self._checked_urls: 650 + return "already checked these URLs. stop guessing and respond with what you know." 651 + if len(self._checked_urls) > 10: 652 + return ( 653 + f"you've already checked {len(self._checked_urls)} URLs in this conversation. " 654 + "stop guessing slugs — you cannot browse the web. tell the user you " 655 + "couldn't find the exact page and share what you found instead." 656 + ) 645 657 646 658 async def _check(client: httpx.AsyncClient, url: str) -> str: 647 659 if not url.startswith(("http://", "https://")): 648 660 url = f"https://{url}" 661 + self._checked_urls.add(url) 649 662 try: 650 663 hostname = urlparse(url).hostname 651 664 if not hostname: ··· 835 848 ) -> Response: 836 849 """Process a mention with structured memory context.""" 837 850 logger.info(f"processing mention from @{author_handle}: {mention_text[:80]}") 851 + self._checked_urls.clear() 838 852 839 853 deps = PhiDeps( 840 854 author_handle=author_handle, ··· 857 871 async with contextlib.AsyncExitStack() as stack: 858 872 for ts in toolsets: 859 873 await stack.enter_async_context(ts) 860 - result = await self.agent.run( 861 - user_prompt, 862 - deps=deps, 863 - toolsets=toolsets, 864 - usage_limits=UsageLimits(request_limit=15), 865 - ) 874 + result = await self.agent.run(user_prompt, deps=deps, toolsets=toolsets) 866 875 logger.info( 867 876 f"agent decided: {result.output.action}" 868 877 + (f" - {result.output.text[:80]}" if result.output.text else "") ··· 883 892 async def process_reflection(self, last_post_text: str | None = None) -> Response: 884 893 """Generate a daily reflection post from recent memory.""" 885 894 logger.info("processing daily reflection") 895 + self._checked_urls.clear() 886 896 887 897 # Pre-fetch context that doesn't benefit from semantic search against the prompt 888 898 recent_activity = "" ··· 941 951 async with contextlib.AsyncExitStack() as stack: 942 952 for ts in toolsets: 943 953 await stack.enter_async_context(ts) 944 - result = await self.agent.run( 945 - reflection_task, 946 - deps=deps, 947 - toolsets=toolsets, 948 - usage_limits=UsageLimits(request_limit=15), 949 - ) 954 + result = await self.agent.run(reflection_task, deps=deps, toolsets=toolsets) 950 955 951 956 logger.info( 952 957 f"reflection decided: {result.output.action}"