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.

inject phi self-identity (handle/did/pds) into system prompt

caught when phi wrote a blog post claiming its memory lives on
"pds.zzstoatzz.io" — that's the operator's PDS, not phi's. phi's
actual PDS is psathyrella.us-west.host.bsky.network (bluesky-hosted).
phi was guessing because nothing in its system prompt told it which
infrastructure was its own.

fix: dynamic system prompt that returns a small "[YOUR INFRASTRUCTURE]"
block with handle, DID, and PDS endpoint. PDS is resolved from the
DID document via the PLC directory on first call, then cached for
the lifetime of the process (it doesn't change between deploys). the
block also includes a one-line note that this is phi's pds, not the
operator's, since that was the specific confusion observed.

scope is intentionally tight — three identity facts plus one
anti-confusion note. the block is ~5 lines and only attached to the
main agent (extraction/exploration agents don't write about phi's
own infrastructure, so they don't need it).

implementation:
- new get_identity_block() in core/atproto_client.py with module-level
cache and httpx call to plc.directory
- new inject_identity dynamic system prompt registered in PhiAgent.__init__

zzstoatzz b285e35f a338624f

+66
+5
src/bot/agent.py
··· 12 12 from pydantic_ai.mcp import MCPServerStreamableHTTP 13 13 14 14 from bot.config import settings 15 + from bot.core.atproto_client import get_identity_block 15 16 from bot.core.curiosity_queue import claim, complete, enqueue, fail 16 17 from bot.core.graze_client import GrazeClient 17 18 from bot.exploration import EXPLORATION_SYSTEM_PROMPT, ExplorationResult ··· 150 151 ) 151 152 152 153 # --- dynamic system prompts --- 154 + 155 + @self.agent.system_prompt(dynamic=True) 156 + async def inject_identity() -> str: 157 + return await get_identity_block() 153 158 154 159 @self.agent.system_prompt(dynamic=True) 155 160 def inject_today() -> str:
+61
src/bot/core/atproto_client.py
··· 1 1 import logging 2 2 from pathlib import Path 3 3 4 + import httpx 4 5 from atproto import Client, Session, SessionEvent 5 6 from atproto_client import models 6 7 ··· 260 261 261 262 262 263 bot_client: BotClient = BotClient() 264 + 265 + 266 + # --- self-identity block (cached) --- 267 + 268 + _identity_block_cache: str | None = None 269 + 270 + 271 + async def get_identity_block() -> str: 272 + """Build phi's self-identity block for the system prompt. 273 + 274 + Resolves the PDS endpoint from the DID document via the PLC directory 275 + on first call, then caches for the lifetime of the process. Phi's PDS 276 + doesn't change between deploys, so a process-lifetime cache is fine. 277 + 278 + The block prevents phi from confusing its own infrastructure with the 279 + operator's — a real failure mode caught when phi wrote a blog post 280 + claiming its memory lived on `pds.zzstoatzz.io` (the operator's PDS, 281 + not phi's). 282 + """ 283 + global _identity_block_cache 284 + cached = _identity_block_cache 285 + if cached is not None: 286 + return cached 287 + 288 + await bot_client.authenticate() 289 + handle = settings.bluesky_handle 290 + me = bot_client.client.me 291 + did = me.did if me else "unknown" 292 + 293 + pds: str | None = None 294 + if did != "unknown": 295 + try: 296 + async with httpx.AsyncClient(timeout=10) as http: 297 + r = await http.get(f"https://plc.directory/{did}") 298 + r.raise_for_status() 299 + doc = r.json() 300 + for svc in doc.get("service", []): 301 + if svc.get("type") == "AtprotoPersonalDataServer": 302 + pds = svc.get("serviceEndpoint") 303 + break 304 + except Exception as e: 305 + logger.warning(f"failed to resolve pds endpoint for {did}: {e}") 306 + 307 + lines = [ 308 + "[YOUR INFRASTRUCTURE]", 309 + f"handle: @{handle}", 310 + f"did: {did}", 311 + ] 312 + if pds: 313 + lines.append(f"pds: {pds}") 314 + lines.append( 315 + "this is YOUR pds, not the operator's. your records — observations, " 316 + "exchanges, blog posts, queue items — live here. the operator runs " 317 + "their own pds elsewhere; don't conflate them." 318 + ) 319 + 320 + block = "\n".join(lines) 321 + _identity_block_cache = block 322 + logger.info(f"identity block built: {handle} / {did} / {pds or 'unknown pds'}") 323 + return block