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.

synthesize episodic memory before injection — top-K + haiku, given goals + query

phi was getting raw top-K episodic notes dumped into its prompt as
[PHI'S RELEVANT MEMORIES]. Stale "pending X" notes appeared next to
fresh ones with equal weight, no synthesis, no contradiction-flagging.

Now: top-K from the vector store, then a haiku pass that takes phi's
current goals + the query as context and produces a coherent, deduped,
recency-aware block. Same shape as [STRANGER'S AUDIT] for posts.

The haiku can flag stale/contradictory entries ("pending follow X" vs.
the actual follow record) without requiring a parallel cleanup
pipeline. Phi can still verify with tools when it matters.

Block renamed to [RELEVANT MEMORIES — synthesized for this query] so
phi knows it's processed, not raw.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+96 -10
+1 -1
loq.toml
··· 17 17 18 18 [[rules]] 19 19 path = "src/bot/memory/namespace_memory.py" 20 - max_lines = 894 20 + max_lines = 945 21 21 22 22 [[rules]] 23 23 path = "src/bot/main.py"
+9 -1
src/bot/agent.py
··· 13 13 from bot.config import settings 14 14 from bot.core.atproto_client import bot_client, get_identity_block 15 15 from bot.core.curiosity_queue import claim, complete, enqueue, fail 16 + from bot.core.goals import list_goals as list_goal_records 16 17 from bot.core.graze_client import GrazeClient 17 18 from bot.core.self_state import get_state_block 18 19 from bot.exploration import EXPLORATION_SYSTEM_PROMPT, ExplorationResult ··· 249 250 query = _extract_query_text(ctx.prompt) 250 251 if not query: 251 252 return "" 253 + # Pass phi's goals so the synthesis can rank by relevance to intent. 254 + try: 255 + goals = await list_goal_records(bot_client) 256 + except Exception: 257 + goals = [] 252 258 try: 253 - episodic_context = await ctx.deps.memory.get_episodic_context(query) 259 + episodic_context = await ctx.deps.memory.get_episodic_context( 260 + query, goals=goals 261 + ) 254 262 if episodic_context: 255 263 return episodic_context 256 264 except Exception as e:
+86 -8
src/bot/memory/namespace_memory.py
··· 7 7 from typing import ClassVar 8 8 9 9 from openai import AsyncOpenAI 10 + from pydantic_ai import Agent 10 11 from turbopuffer import Turbopuffer 11 12 12 13 from bot.config import settings ··· 19 20 ) 20 21 21 22 logger = logging.getLogger("bot.memory") 23 + 24 + 25 + # Lazy haiku agent — synthesizes top-K episodic candidates into a coherent 26 + # block, given phi's goals + the current query as context. Replaces a raw 27 + # top-K dump that was producing stale/contradictory content alongside fresh. 28 + _episodic_synth_agent: Agent | None = None 29 + 30 + 31 + def _get_episodic_synth_agent() -> Agent: 32 + global _episodic_synth_agent 33 + if _episodic_synth_agent is None: 34 + _episodic_synth_agent = Agent[None, str]( 35 + name="phi-episodic-synth", 36 + model=settings.extraction_model, 37 + system_prompt=( 38 + "You're helping phi pull a tight, useful summary from its " 39 + "episodic memory for the situation at hand. You'll see phi's " 40 + "current goals, what phi is processing right now, and the " 41 + "raw candidates retrieved by similarity from the vector " 42 + "store.\n\n" 43 + "Write only what helps phi act on the current query. Dedupe " 44 + "near-identical entries. Prefer recent over stale when they " 45 + "conflict. Flag entries that may be stale (e.g. 'pending X' " 46 + "notes about actions that may have completed since) — phi " 47 + "can verify with tools if it matters.\n\n" 48 + "Lowercase. No preamble, no meta-commentary. If nothing in " 49 + "the candidates is actually relevant, return an empty string." 50 + ), 51 + output_type=str, 52 + ) 53 + return _episodic_synth_agent 54 + 55 + 56 + async def _synthesize_episodic( 57 + goals: list[dict], query: str, raw_notes: list[dict] 58 + ) -> str: 59 + if not raw_notes: 60 + return "" 61 + 62 + if goals: 63 + goals_block = "\n".join( 64 + f"- {g.get('title', '')}: {g.get('description', '')}" for g in goals 65 + ) 66 + else: 67 + goals_block = "(no goals set)" 68 + 69 + notes_block = "\n".join( 70 + f"[{(n.get('created_at') or '')[:10]}] {n.get('content', '')}" 71 + for n in raw_notes 72 + ) 73 + 74 + payload = ( 75 + f"phi's current goals:\n{goals_block}\n\n" 76 + f"what phi is processing right now:\n{query}\n\n" 77 + f"raw episodic candidates (top {len(raw_notes)} by similarity):\n" 78 + f"{notes_block}" 79 + ) 80 + 81 + try: 82 + result = await _get_episodic_synth_agent().run(payload) 83 + return (result.output or "").strip() 84 + except Exception as e: 85 + logger.warning(f"episodic synthesis failed: {e}") 86 + return "" 22 87 23 88 24 89 class NamespaceMemory: ··· 463 528 return [] 464 529 raise 465 530 466 - async def get_episodic_context(self, query_text: str, top_k: int = 5) -> str: 467 - """Get formatted episodic context for injection into conversation prompt.""" 468 - results = await self.search_episodic(query_text, top_k=top_k) 469 - if not results: 531 + async def get_episodic_context( 532 + self, 533 + query_text: str, 534 + goals: list[dict] | None = None, 535 + top_k: int = 10, 536 + ) -> str: 537 + """Get a haiku-synthesized episodic context block for the prompt. 538 + 539 + Top-K from the vector store, then a synthesis pass that takes phi's 540 + goals + the current query as context and produces a coherent 541 + block (deduped, recency-aware, contradictions flagged) instead of 542 + a raw dump of similarity-ranked notes. 543 + 544 + Returns an empty string if there are no relevant candidates. 545 + """ 546 + raw = await self.search_episodic(query_text, top_k=top_k) 547 + if not raw: 470 548 return "" 471 - lines = ["[PHI'S RELEVANT MEMORIES]"] 472 - for r in results: 473 - lines.append(f"- {r['content']}") 474 - return "\n".join(lines) 549 + summary = await _synthesize_episodic(goals or [], query_text, raw) 550 + if not summary: 551 + return "" 552 + return f"[RELEVANT MEMORIES — synthesized for this query]\n{summary}" 475 553 476 554 async def search_unified( 477 555 self, handle: str, query: str, top_k: int = 8