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 list_blog_posts uri output + make process_* resilient to agent.run failures

two related bugs caught when phi tried to respond to a smoke test
mention asking for a long-form reintroduction:

1. list_blog_posts returned only the human-facing greengale URL
("https://greengale.app/{handle}/{rkey}"), not the AT-URI. when
phi (the model) wanted to read one of those posts back via
pub_get_document — which requires an AT-URI — she had to translate
url -> at-uri on the fly and guess the collection name. she
guessed "pub.greengale.article". the actual collection on her PDS
is "app.greengale.document". 100% reproducible failure for any
list-then-read flow.

fix: include the AT-URI explicitly in list_blog_posts output
alongside the URL. now the model passes it directly to
pub_get_document, no translation needed.

2. process_mention propagated agent.run exceptions all the way out
to the notification handler, which logged it and posted nothing.
from the operator's view phi looked like she was ignoring people.
when a tool error happens she should surface it, not vanish.

fix: wrap agent.run in process_mention with try/except. on failure,
return a fallback Response that posts a brief honest reply tagging
the operator so the failure gets noticed. process_reflection and
process_musing get the same wrapping but return action='ignore'
instead — they're scheduled and there's no human waiting.

process_exploration was already wrapped (queue items get fail()'d
on exception), so no change needed there.

zzstoatzz a338624f 61bf9f86

+54 -13
+49 -12
src/bot/agent.py
··· 294 294 # the reference count instead of opening/closing across tasks. 295 295 # https://github.com/pydantic/pydantic-ai/issues/2818 296 296 toolsets = self._mcp_toolsets() 297 - async with contextlib.AsyncExitStack() as stack: 298 - for ts in toolsets: 299 - await stack.enter_async_context(ts) 300 - result = await self.agent.run(user_prompt, deps=deps, toolsets=toolsets) 297 + try: 298 + async with contextlib.AsyncExitStack() as stack: 299 + for ts in toolsets: 300 + await stack.enter_async_context(ts) 301 + result = await self.agent.run(user_prompt, deps=deps, toolsets=toolsets) 302 + except Exception as e: 303 + # Don't go silent on tool/agent failures — surface to the operator. 304 + # Phi posts a brief honest reply tagging the owner so the failure 305 + # gets noticed instead of disappearing into a log line. 306 + err_type = type(e).__name__ 307 + err_msg = str(e)[:200] 308 + logger.exception( 309 + f"agent.run failed for mention from @{author_handle}: {err_type}" 310 + ) 311 + return Response( 312 + action="reply", 313 + text=( 314 + f"hit a tool error mid-response and dropped out so i don't compound it. " 315 + f"@{settings.owner_handle} this needs your attention — check the logs." 316 + ), 317 + reason=f"{err_type}: {err_msg}", 318 + ) 319 + 301 320 logger.info( 302 321 f"agent decided: {result.output.action}" 303 322 + (f" - {result.output.text[:80]}" if result.output.text else "") ··· 373 392 ) 374 393 375 394 toolsets = self._mcp_toolsets() 376 - async with contextlib.AsyncExitStack() as stack: 377 - for ts in toolsets: 378 - await stack.enter_async_context(ts) 379 - result = await self.agent.run(reflection_task, deps=deps, toolsets=toolsets) 395 + try: 396 + async with contextlib.AsyncExitStack() as stack: 397 + for ts in toolsets: 398 + await stack.enter_async_context(ts) 399 + result = await self.agent.run( 400 + reflection_task, deps=deps, toolsets=toolsets 401 + ) 402 + except Exception as e: 403 + err_type = type(e).__name__ 404 + logger.exception(f"agent.run failed during reflection: {err_type}") 405 + return Response( 406 + action="ignore", 407 + reason=f"reflection {err_type}: {str(e)[:200]}", 408 + ) 380 409 381 410 logger.info( 382 411 f"reflection decided: {result.output.action}" ··· 426 455 ) 427 456 428 457 toolsets = self._mcp_toolsets() 429 - async with contextlib.AsyncExitStack() as stack: 430 - for ts in toolsets: 431 - await stack.enter_async_context(ts) 432 - result = await self.agent.run(musing_task, deps=deps, toolsets=toolsets) 458 + try: 459 + async with contextlib.AsyncExitStack() as stack: 460 + for ts in toolsets: 461 + await stack.enter_async_context(ts) 462 + result = await self.agent.run(musing_task, deps=deps, toolsets=toolsets) 463 + except Exception as e: 464 + err_type = type(e).__name__ 465 + logger.exception(f"agent.run failed during musing: {err_type}") 466 + return Response( 467 + action="ignore", 468 + reason=f"musing {err_type}: {str(e)[:200]}", 469 + ) 433 470 434 471 logger.info( 435 472 f"musing decided: {result.output.action}"
+5 -1
src/bot/tools/blog.py
··· 43 43 url = f"https://greengale.app/{handle}/{rkey}" 44 44 tag_str = f" [{', '.join(tags)}]" if tags else "" 45 45 date_str = f" ({published[:10]})" if published else "" 46 - lines.append(f"- {title}{tag_str}{date_str}\n {url}") 46 + # include the AT-URI explicitly so the model doesn't have to guess 47 + # the collection name when passing to pub_get_document. 48 + lines.append( 49 + f"- {title}{tag_str}{date_str}\n uri: {rec.uri}\n url: {url}" 50 + ) 47 51 return "\n".join(lines) 48 52 except Exception as e: 49 53 return f"failed to list blog posts: {e}"