<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Hippogriff</title>
    <link>https://www.hippogriff.io</link>
    <atom:link href="https://www.hippogriff.io/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Posts on software engineering, AI, and agents by Vicki Zhang.</description>
    <language>en</language>
    <item>
      <title>Reading Anthropic's Memory API</title>
      <link>https://www.hippogriff.io/blog/reading-anthropics-memory-api</link>
      <guid isPermaLink="true">https://www.hippogriff.io/blog/reading-anthropics-memory-api</guid>
      <pubDate>Wed, 29 Apr 2026 00:00:00 GMT</pubDate>
      <description>Anthropic's Memory API elevates file-based memory into a distributed primitive: per-file linearizability via compare-and-swap. Local filesystem to the agent, distributed semantics underneath. My best guess at the design.</description>
      <content:encoded><![CDATA[<p>Anthropic's <a href="https://claude.com/blog/claude-managed-agents-memory">Memory API</a> elevates file-based memory into a distributed primitive: per-file linearizability via compare-and-swap (CAS). The agent sees a filesystem. The system handles consistency.</p>
<p>This post is my best guess at the design. Public docs cover the surface: mounts, access modes, optimistic concurrency. The internals are inferred. The framing is the point; specific implementation claims may be wrong.</p>
<h2 id="why-files">Why files</h2>
<p>Files are the right primitive because models have got better at managing them. From <a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills">Anthropic's own framing</a>: <em>"Claude Code can accomplish complex tasks across domains using local code execution and filesystems."</em> <a href="https://x.com/RLanceMartin/status/2047720067107033525">Lance Martin also added that</a>: files are shareable and interpretable in ways embeddings are not.</p>
<p>There's also the challenge to get RAG right: selecting an embedding model, tuning chunk sizes, versioning the index. As model capability grows, file-based memory is the high-ROI choice. The agent does the curation, after all, why waste the model's ability?</p>
<h2 id="how-the-agent-sees-memory">How the agent sees memory</h2>
<p>When you attach a memory store to a session, it mounts at <code>/mnt/memory/&#x3C;store-name>/</code> and a short note about the mount goes into the system prompt. That's how an agent becomes aware of which stores are available. The agent doesn't need a special tool. <code>read_file</code> and <code>grep</code> work because the mount is represented as a directory.</p>
<p><a href="https://platform.claude.com/docs/en/managed-agents/memory#how-the-agent-accesses-memory">The docs recommend</a> keeping files under 100KB and splitting context across many focused files instead of one big one. <code>grep</code> finds the right file. <code>read_file</code> pulls only what's needed. Context stays lean.</p>
<h2 id="safety">Safety</h2>
<p>Read-only mounts are the answer to prompt injection on shared memory. From <a href="https://platform.claude.com/docs/en/managed-agents/memory#manage-memory-stores">the docs</a>: <em>"if the agent processes untrusted input ... a successful prompt injection could write malicious content into the store. Later sessions then read that content as trusted memory."</em> Access mode is set at session creation, enforced at runtime.</p>
<p>Every change generates a new version of the store. The audit trail is built in. You don't add it later.</p>
<h2 id="my-guess-at-the-design">My guess at the design</h2>
<p><strong>Data model.</strong> Two levels: a memory store, which is the workspace-scoped unit, and individual memories (files) inside it. Versioning sits at the store level for snapshots, but linearizability is at the file level.</p>
<p><img src="https://www.hippogriff.io/blog/data_model_memory.png" alt="data_model"></p>
<p><strong>Write consistency.</strong> The docs phrase it as <em>"safe content edits (optimistic concurrency)"</em>. That reminds me of CAS. Same pattern <a href="https://aws.amazon.com/blogs/storage/building-multi-writer-applications-on-amazon-s3-using-native-controls/">S3 ships for multi-writer apps</a>: each write carries the expected version; if the version moved, the write fails and the agent retries. This effectively prevents lost writes.</p>
<p><img src="https://www.hippogriff.io/blog/architecture_memory.png" alt="memory_architecture"></p>
<p><strong>Read consistency.</strong> Eventual. My guess: the store mounts lazily into the sandbox. First read on a file pulls bytes; subsequent reads serve from a local cache so grep stays fast. The cache invalidates on local writes. For <em>other</em> sessions' writes you'd need explicit polling or invalidation. The trade-off: cross-session writes aren't visible until the cache refreshes.</p>
<p>In my opinion that's fine. A memory entry going stale for a couple minutes should not materially change answer quality. Linearizability matters at write time. Reads can be looser.</p>
<p><strong>Version log.</strong> Written once, read rarely. Append-only fits. Blobs live in S3, with log entries carrying <code>version_id</code>, <code>parent_version_id</code>, <code>blob_hash</code>, <code>operation</code>. S3 keys by content hash, supporting <code>memory_versions.list</code> would be inefficient if relying on S3 alone. So it makes sense to introduce a Dynamo DB to store secondary index, which is derived from the S3 blobs, makes looksup by <code>store_id</code> or <code>memory_id</code> fast.</p>
<p>Specifically, for DynamoDB, a partition key on <code>store_id</code> matches the access pattern: list memories by store, fast.</p>
<p><strong>Multi-region.</strong> The most plausible path: S3 cross-region replication fires a notification on blob arrival, an event handler in the secondary region reads the blob and writes the local secondary index. Same shape as primary, just driven by replication events.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How Agents Communicate Inside A Team</title>
      <link>https://www.hippogriff.io/blog/how-agents-communicate-inside-a-team</link>
      <guid isPermaLink="true">https://www.hippogriff.io/blog/how-agents-communicate-inside-a-team</guid>
      <pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate>
      <description>I dug into Claude Code's .claude folder to understand how agents talk to each other. Then I rebuilt it from scratch in ~320 lines of Python using the Claude Agent SDK.</description>
      <content:encoded><![CDATA[<p>I build workflow AI agent at work. Permissioning is muscle memory: who can call what, which service talks to which. So when I set up a 6-agent <code>laser-team</code> debate system (lead, judge, two pairs of debaters debating from two different aspects) in Claude Code for ideation and told two pairs to stay isolated, my first instinct was to check: how is this enforced?</p>
<p>It's not. (Seriously, if I'm wrong about this, tell me.)</p>
<p>I dug around in the <code>.claude</code> folder. No allowlist, no routing table, no config for routing rules. The entire communication boundary is prompt instruction.</p>
<p>That intrigued me. So I mimicked one from scratch: a <a href="https://github.com/hippogriff-ai/concept-demos/tree/main/nano_team">minimal 4-agent team</a> using the Claude Agent SDK. One lead, two debaters, one judge, filesystem mailboxes. ~320 lines of Python. The debaters argue, submit final positions to the judge (not the lead), and the judge sends a verdict. The chain of command exists only in the prompts.</p>
<p>Here's what I found.</p>
<h2 id="agents-talk-through-files-on-disk">Agents talk through files on disk</h2>
<p>The key routing constraint: debaters submit to the judge, not the lead. Going over the judge undermines the process.</p>
<p>No API. No message broker. Agents write JSON to each other's inbox files:</p>
<pre><code>output/
├── config.json              # who's on the team
├── inboxes/
│   ├── lead.json            # messages TO the lead
│   ├── teammate-for.json
│   ├── teammate-against.json
│   └── judge.json
└── tasks/
    └── 1.json               # the debate task
</code></pre>
<p>When teammate-for wants to message teammate-against, it appends JSON to <code>teammate-against.json</code> in the inbox. That's it. Architecturally, it's a Slack workspace. Anyone CAN message anyone.</p>
<p>Here's what it looks like right after spawning. All four inboxes empty, 9 sub-tasks already created by the teammates, everyone starting concurrently:</p>
<p><img src="https://www.hippogriff.io/blog/nano-team-inbox-empty.png" alt="VS Code showing four empty inbox files, 9 task files in the sidebar, and the terminal with all three teammates spawning in background"></p>
<h2 id="routing-by-prompt-alone">Routing by prompt alone</h2>
<p>The <code>send_message</code> tool has no routing logic:</p>
<pre><code class="language-python">@tool("send_message", "Send a message to any agent by name",
      {"recipient": str, "text": str, "summary": str, "message_type": str})
async def send_message(args):
    # No allowlist. Any agent can message anyone. Routing is prompt-only.
    msg = {"from": owner, "to": args["recipient"], ...}
    path = INBOXES / f"{args['recipient']}.json"
    msgs = json.loads(path.read_text())
    msgs.append(msg)
    path.write_text(json.dumps(msgs, indent=2))
</code></pre>
<p>No validation on <code>recipient</code>. A debater told to submit to the judge can just as easily message the lead directly. The routing I want exists only in the prompts:</p>
<pre><code class="language-python"># This is MY mental model. send_message has zero awareness of it.
allowed = {
    "lead":             {"teammate-for", "teammate-against", "judge"},
    "teammate-for":     {"teammate-against", "judge"},  # NOT lead
    "teammate-against": {"teammate-for", "judge"},      # NOT lead
    "judge":            {"lead"},                        # verdict only
}
</code></pre>
<p>In my demo, the routing held. Every run, zero breaches. I ran the same check on 5 real Claude Code laser-team sessions, different topics, scanned every inbox file. Zero cross-pair violations there too.</p>
<p>The prompt held every time. I'm not sure if this is because the model is really good at instruction following, or if there's something else going on that I haven't found yet.</p>
<h2 id="the-lead-is-also-an-llm">The lead is also an LLM</h2>
<p>The lead is not a Python script. It's another Claude session with tools: <code>spawn_teammate</code>, <code>send_message</code>, <code>create_task</code>, <code>read_inbox</code>. It decides when to spawn, when to judge, when to shut down.</p>
<p>Its entire instruction is a prompt:</p>
<pre><code>1. Create a debate task
2. Spawn teammate-for (runs in background)
3. Spawn teammate-against (runs in background)
4. Spawn judge (runs in background)
5. Keep reading your inbox until judge sends the verdict
6. Shut down all teammates
7. Present the verdict
</code></pre>
<h2 id="agents-poll-for-mail">Agents poll for mail</h2>
<p>All teammates start concurrently. They don't know about each other. They discover the other agents by checking their inbox.</p>
<p>Here is the magical part: files acting as inboxes that take messages from other agents. <code>read_inbox</code> polls the file for changes. If nothing's new, it waits up to 30 seconds before returning.</p>
<p>I learned this the hard way. With a short wait time, each empty poll burned a turn. The lead hit its turn limit and declared the debate over before anyone had finished.</p>
<p>Claude Code's architecture uses the same poll-based approach for inbox delivery.</p>
<p>Mid-debate. All teammates running concurrently, messages flowing through inboxes, tasks being created and claimed:</p>
<p><img src="https://www.hippogriff.io/blog/nano-team-mid-debate.png" alt="VS Code mid-debate: all three inbox files filling with messages, 9 task files created, teammates exchanging arguments in the terminal"></p>
<h2 id="shutdown-is-just-another-message">Shutdown is just another message</h2>
<p>No special shutdown tool. The lead sends <code>send_message</code> with <code>message_type="shutdown_request"</code>. Same channel as debate messages.</p>
<pre><code class="language-python">send_message(recipient="teammate-for", text="Shutting down",
             summary="shutdown", message_type="shutdown_request")
</code></pre>
<p>The teammate reads it from their inbox like any other message. In real Claude Code teams, approving shutdown kills the process. In my demo, I tell the LLM "your process is terminating, do not call any more tools." In one run, a teammate approved shutdown but kept sending a rebuttal anyway. Shutdown seems to be cooperative, not enforced.</p>
<p>Here's the final state. judge.json has both final positions from the debaters, lead.json has the judge's structured verdict, and the forensic audit shows zero breaches:</p>
<p><img src="https://www.hippogriff.io/blog/nano-team-shutdown.png" alt="VS Code after shutdown: four inbox files showing the complete message flow, terminal with forensic audit showing no routing breaches, all 10 tasks done"></p>
<p><em>Debaters sent to judge, not lead. Judge sent verdict to lead. Zero routing breaches.</em></p>
<h2 id="adding-one-agent-doubled-the-time">Adding one agent doubled the time</h2>
<p>I originally built this with 3 agents. The lead judged the debate directly. No routing constraint to test. Finished in ~2 minutes. (<a href="https://youtu.be/LASbM6fcdbI">3-agent run</a>)</p>
<p>So I added a 4th agent (the judge) to create a real routing constraint: can I make debaters submit to the judge instead of the lead? The routing held. But the run took ~4 minutes. (<a href="https://youtu.be/v5pETaNlpEA">4-agent run</a>)</p>
<p>Frankly I cannot pinpoint the exact reason why it took so much longer (oh well, I cut corners here not adding langsmith tracing ...), what I know is that the prompts are almost identical, one more agent was added.</p>
<h2 id="task-status-is-soft-too">Task status is soft too</h2>
<p>Teammates create and update tasks as they work. In the run above, they created 9 sub-tasks organically: "Research arguments FOR", "Write rebuttal", "Send final position to judge", etc. When a task is claimed, its status gets updated. When it's done, the teammate marks it complete.</p>
<p>But nothing enforces this. The <a href="https://code.claude.com/docs/en/agent-teams">Claude Code docs</a> warn: "Task status can lag. Teammates sometimes fail to mark tasks as completed." In earlier runs, I saw tasks left as <code>"in_progress"</code> after the debate was fully finished. The work completed fine. The task board just didn't reflect it.</p>
<h2 id="try-it">Try it</h2>
<p><a href="https://github.com/hippogriff-ai/concept-demos/tree/main/nano_team">hippogriff-ai/concept-demos/nano_team</a>. One file, runs in ~4 minutes:</p>
<pre><code class="language-bash">cd nano_team
cp .env.example .env  # add your Anthropic API key
uv sync
uv run python nano_team.py --trace "Your debate topic"
</code></pre>
<p>Open <code>output/</code> in your IDE while it runs. The filesystem is the ground truth. Every message, every forgotten task update, it's all in those JSON files.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Remo: From Moldy Bathroom to Shoppable Redesign, All AI-Orchestrated</title>
      <link>https://www.hippogriff.io/blog/remo-lidar-room-redesign-agent</link>
      <guid isPermaLink="true">https://www.hippogriff.io/blog/remo-lidar-room-redesign-agent</guid>
      <pubDate>Wed, 18 Feb 2026 00:00:00 GMT</pubDate>
      <description>Built an iOS app at the Claude Code x Cerebral Valley hackathon — photograph your room, scan with LiDAR, chat with an AI design consultant, get photorealistic redesigns, and walk away with a shoppable product list.</description>
      <content:encoded><![CDATA[<p><em>Project for '<a href="https://cerebralvalley.ai/e/claude-code-hackathon">Built with Opus 4.6</a>' Claude Code x Cerebral Valley hackathon</em></p>
<p>Grateful to be picked as one of 500 vibe coders from 13K applicants. Cool to have met fellow builders and learned from them. It was a lot of fun.</p>
<div class="video-embed"><iframe src="https://www.youtube.com/embed/cwqyoOuIJHI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>
<p>Our bathroom has mold — the kind where friends gently suggest that maybe that's why we're always tired. Hiring a contractor was inevitable, but the design part? Out of budget. Plus, I've always loved flipping through Architectural Digest and imagining what I'd do with a space. So when the <a href="https://cerebralvalley.ai/e/claude-code-hackathon">Built with Opus 4.6 hackathon</a> opened up, I built the tool, something I had been thinking about but hadn't gotten around to.</p>
<p><a href="https://github.com/hippogriff-ai/remo">Remo</a> is an iOS app where you photograph your room, scan it with LiDAR, chat with an AI design consultant, get photorealistic redesigns, iteratively refine them by annotating what you want changed, and walk away with a shoppable product list. Photo to purchasable product list, all AI-orchestrated.</p>
<p>Here's what I learned building it.</p>
<h2 id="architecture">Architecture</h2>
<p><img src="https://www.hippogriff.io/blog/blog-architecture.svg" alt="Architecture"></p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td>Claude Opus 4.6</td>
<td>Intake chat, room analysis, shopping extraction, VLM eval judge, intake eval judge</td>
</tr>
<tr>
<td>Claude Sonnet 4.5</td>
<td>Scoring products found in search step</td>
</tr>
<tr>
<td>Claude Haiku 4.5</td>
<td>Photo validation (is this a room photo?)</td>
</tr>
<tr>
<td>Gemini 3 Pro Image</td>
<td>Image generation, multi-turn editing</td>
</tr>
</tbody>
</table>
<p>One Temporal workflow per design project. The user's journey — upload photos, scan the room, chat about style, generate designs, annotate changes, approve, get a shopping list — is one long durable execution. If Gemini times out mid-generation, Temporal retries automatically. If the user closes the app and comes back tomorrow, their progress is still there. If they abandon entirely, a 48-hour timeout auto-purges the entire project from R2 and the database. No cron jobs. No orphaned data.</p>
<p><img src="https://www.hippogriff.io/blog/blog-user-journey.svg" alt="User Journey"></p>
<p>The key architectural rule: <strong>AI calls only happen in activities</strong> (except one sync photo validation in the API). <strong>Workflow handles orchestration. Activities are stateless</strong> — a deliberate trade-off. Every activity re-fetches what it needs (re-downloading images from R2 on every retry) instead of caching in memory. That costs extra I/O, but it buys durability: if a worker crashes mid-generation, Temporal retries the activity on any available worker with zero state to reconstruct. I chose reliability over efficiency because the expensive part is the AI call, not a second image download.</p>
<p>This separation also meant I could swap Gemini model versions, change prompt strategies, and add entire pipeline stages without touching the workflow state machine. Tests across backend and iOS verify this contract holds.</p>
<h2 id="read-the-room-eager-analysis">"Read the Room!" — Eager Analysis</h2>
<p>Users spend 30-60 seconds on the LiDAR scan. Dead time for the server. So while they walk around, Claude Opus reads their photos and builds a design hypothesis — room type, furniture inventory (keep or replace?), behavioral signals ("toiletries everywhere" → "needs concealed storage"), and where the biggest opportunity lives.</p>
<p>Result: the AI's first question isn't "what style do you like?" — it's "I notice you have no concealed storage — is that a pain point?"</p>
<p>The workflow fires this via <code>asyncio.shield()</code> concurrently with the scan. Opus is thorough, so it sometimes finishes after the user. If it fails entirely? Blank-slate conversation. Less intelligent first question, no crash.</p>
<h2 id="the-intake-agent-not-a-form-a-design-brain">The Intake Agent: Not a Form. A Design Brain.</h2>
<p>Most AI apps ask you to fill out a form. Remo has a conversation. The agent asks about <em>your</em> room, references what it saw in the photos, and translates vague language ("I want it cozy") into professional design language ("warm 2700K ambient lighting with layered textiles"). Say "I want it cozy" and the agent dynamically loads style-specific knowledge — proportions, material palettes, signature elements — into its next response.</p>
<p>Behind this is a 243-line system prompt encoding three layers of interior design theory: Francis Ching's spatial foundations, Elsie de Wolfe's human-centered refinement, and Dorothy Draper's emotional impact principles. I didn't know any of these names before building Remo. A designer friend pointed me to the icons, and I spent hours having Claude give me a crash course on the domain — while Claude Code was Ralph-looping Maestro UI tests on the side.</p>
<p><img src="https://www.hippogriff.io/blog/image-1.png" alt="Intake agent"></p>
<p>The constraint that makes it work: the agent can only do two things each turn — ask a question or produce a design brief. No free-form rambling. Every turn either gathers information or delivers a result. When the turn budget hits 1, the prompt literally says: "This IS your last chance — draft the best brief you can."</p>
<p>The output is a <code>DesignBrief</code> — room type, pain points, keep items, lighting with Kelvin temps, colors with 60/30/10 proportions, textures as material descriptors, lifestyle patterns, emotional drivers. Every field translates directly into the image generation prompt. The conversation streams to the iOS client via SSE in real-time, so the user watches it appear word by word.</p>
<h2 id="the-annotation-circle-what-multimodal-models-taught-me-about-data">The Annotation Circle: What Multimodal Models Taught Me About Data</h2>
<p>Users circle areas they want changed. I was trying to be smart, adding annotations into the input picture. Consequently, Gemini kept those circles in the redesigned image.</p>
<p><img src="https://www.hippogriff.io/blog/pic_with_mark.PNG" alt="Annotation circles retained in output"></p>
<p>My first instinct was to fix the prompt. "Ignore the annotation markers." "The circles are instructions, not content." "Do NOT reproduce any overlay elements." Seven versions of increasingly desperate wording. None of them worked.</p>
<p>The learning: <strong>in image generation, visual signals override text instructions.</strong> Telling Gemini "ignore the circles" created a contradiction — the pixels said "circles are here" while the prompt said "pretend they're not." The model couldn't reliably distinguish between "annotation that the user drew" and "decorative element on the wall" because both were just pixels in the input image.</p>
<p>The fix wasn't better wording. It was changing the data representation entirely — stop sending visual annotations, switch to text coordinates: <code>"Region 1: upper-left area of the image (30% from left, 40% from top, medium area) — ACTION: replace — INSTRUCTION: replace the dated vanity with a floating modern one"</code>. No more conflicting signals. Problem gone. The VLM eval's <code>artifact_cleanliness</code> score confirmed the fix quantitatively.</p>
<p>The broader takeaway: when your text prompt and your image contradict each other, <strong>the image wins</strong>. So don't fight the model — change the input.</p>
<h2 id="the-eval-pipeline-measurement-over-intuition">The Eval Pipeline: Measurement Over Intuition</h2>
<p>How do you know if a generated room redesign is any good? You "look" at it.</p>
<p>I built a VLM judge — Claude Opus 4.6 scores every generated image against a 100-point rubric with
9 criteria. Room preservation gets the highest weight (20/100), because if the AI turns your room
into a completely different room, that's useless no matter how pretty it is.</p>
<p><strong>25 versioned prompt files</strong> across three tracks (generation, edit, shopping). A/B testing generates 5 images per version, scores each, then runs <strong>10,000 bootstrap resamples</strong> on those 10 scores — small sample, heavy statistical lifting — and returns a SHIP, LIKELY_BETTER, INCONCLUSIVE, or ROLLBACK verdict. Cost per eval: ~$0.02.</p>
<p>Generation prompt v1 was 15 generic lines. By v5, it references Canon EOS R5 camera
characteristics — because that's what makes renders look like photographs instead of AI slop.</p>
<p>The rollback story: I shipped room_preservation v5 — LiDAR-aware dimensional constraints. The
original eval said "LIKELY_BETTER — SHIP." Then I upgraded the judge from Sonnet 4.5 to Opus 4.6
and built a proper rubric. The stronger judge scored v5 worse than v4 on spatial accuracy.
Rollback was one line in a JSON config.</p>
<p>The lesson: measure your measurement tool too. A weak judge had approved a regression that a stronger judge caught.</p>
<h2 id="the-shopping-pipeline-5-steps-24-retailers-real-products">The Shopping Pipeline: 5 Steps, 24 Retailers, Real Products</h2>
<p>The shopping pipeline extracts items from the approved design (Claude vision), builds multi-query search strategies per item, runs dual-pass Exa searches (retailer-constrained pass + open web), scores candidates with a category-adaptive rubric, and assembles the final list.</p>
<p><strong>28 color synonyms</strong> expand search recall — "sage" also searches "muted green," "olive," "eucalyptus." <strong>24 curated retailer domains</strong> anchor the first pass. A <strong>flag-don't-gate</strong> philosophy: if a product's dimensions might not fit the LiDAR-measured space, it gets a warning badge, not silent removal. Your room, your call.</p>
<p>The whole pipeline streams results via SSE with a 3-second handoff protocol between the Temporal workflow and the API process — if the iOS client is connected and polling, it claims streaming ownership and delivers products in real-time. Otherwise, the workflow runs shopping as a standard Temporal activity and the user sees results all at once.</p>
<h2 id="behind-the-scenes-the-demo-video">Behind the Scenes: The Demo Video</h2>
<p><img src="https://www.hippogriff.io/blog/bathroom-recording-backstage.JPG" alt="Recording setup"></p>
<p>Claude Code co-produced the demo video — 25 SVG overlays from text prompts, burned them into clips via ffmpeg, planned the shot list, drafted voiceover scripts. Even with Claude Code's help, I only managed to submit without voiceover, 2 minutes before deadline. Finished beats perfect. Though I felt a bit disappointed in my video editing fluency - time to shake it off, just means more deliberate practice!</p>
<h2 id="next-steps">Next Steps</h2>
<ul>
<li>
<p><strong>Latency</strong> — The current workflow takes 15min (raw demo video <a href="https://youtu.be/dPx6_dTu0H4">here</a>). To me, it feels way too long. Frankly, I am not sure how to cut down the latency in image generation. Need to do more research.</p>
</li>
<li>
<p><strong>Search relevance</strong> — Better ranking signals and visual similarity to close the gap between design and purchasable products. I truly do not enjoy shopping all night for renovation things where style and measurement both matter and ended up with sore eyes and an empty shopping cart.</p>
</li>
</ul>
<h2 id="takeaway">Takeaway</h2>
<h3 id="durability">Durability</h3>
<p>A good prompt means nothing if the system around it can't retry failures, survive user abandonment, or stream results in real-time. The prompts are maybe 30% of the work. The other 70% is making them reliable at scale.</p>
<h3 id="enclosed-feedback-eval-loop">Enclosed feedback eval loop</h3>
<p>The eval pipeline isn't just for me to check results — it's what lets Claude Code iterate on prompts autonomously. Give it a high-level guideline and an eval rubric, and it can tweak a prompt, run the eval, read the scores, adjust, and try again. That's how 25 prompt versions happened without 28 manual review sessions. Some shipped. Some rolled back. The loop catches regressions that neither I nor the AI would notice in a single pass.</p>
<h3 id="spec">Spec</h3>
<p>Spec is worth investing your time in, even though it means that the coding will start late. It helps you to think deeper, to reflect whether something is worth building, how it should behave, what should be non-negotiable.</p>
<h3 id="my-go-to-claude-code-features">My Go-to claude code features</h3>
<h4>Agent Team</h4>
<p>I like using a team of agents on research and gap analysis tasks.</p>
<h4>Ralph-loop</h4>
<p>Eventual close-enough perfection right? Just like life.</p>
]]></content:encoded>
    </item>
    <item>
      <title>What I learned from building a resume agent</title>
      <link>https://www.hippogriff.io/blog/what-i-learned-from-resume-agent</link>
      <guid isPermaLink="true">https://www.hippogriff.io/blog/what-i-learned-from-resume-agent</guid>
      <pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate>
      <description>Finishing an unfinished business, sharing some learnings on agent design</description>
      <content:encoded><![CDATA[<h2 id="why-i-built-this-resume-agent-talent-promo">Why I built this resume agent (Talent Promo)</h2>
<p>I always feel defeated when revising my resume. The mixed feeling of "I have so much to offer" and "I am not enough" keeps fighting each other. And guess what, my friends do not enjoy the process either.</p>
<p>About a year ago, two other friends and I tried to build a resume agent in a Google Agent SDK hackathon. We abandoned it halfway because of scheduling and other challenges. I really wanted to materialize the vision, therefore started to tinker on it in spare time off and on. Yeah, it took a while, multiple redesigns from agent harness all the way to UX. And today, it is finally in a not-so-unusable state.</p>
<p><a href="https://talent-promo.hippogriff.io">Try here</a> : 30 complimentary runs per day globally</p>
<h2 id="things-i-learned">Things I learned</h2>
<h3 id="workflow-or-agentic">Workflow or agentic</h3>
<p>I chose the workflow design because an orchestrated stage made most sense to me as a user:
Research target and gaps -> Discover hidden power -> Initial Draft -> Iteration Drafting -> Final output</p>
<p>Meanwhile, I keep doubting myself if this is the best way. <a href="http://www.incompleteideas.net/IncIdeas/BitterLesson.html">Bitter lesson</a>, and this <a href="https://x.com/buckeyevn/status/2014171253045960803?s=46">article</a> from Minh Pham mentioned using a human way of thinking to solve problems may restrict the power of LLM. I might go do some experimentation on different agent architectures.</p>
<p>The two architectures on my target list are:</p>
<ul>
<li>main-subagents for efficient context management</li>
<li>skill-based single agent architecture: I do notice that there is a lot of shareable context throughout the workflow. As a matter of fact, I am passing the state along the current orchestrated agent architecture.</li>
</ul>
<h3 id="agentic-runtime">Agentic runtime</h3>
<p>I am a fan of <code>langgraph</code>. The flexibility to define state, control the state flow, and native support of human-in-the-loop thanks to checkpointing has expanded the canvas greatly to build an agent.
<code>deepagents</code> has also been on my radar. I like it a lot for agent harness without tying to a specific model provider. Though I did not use <code>deepagents</code> lib in this project, I want to thank it for teaching me how to think of virtual file system vs. database retrieval. I am amazed at the duck typing that bridged the strong Linux command skill from LLM and RDBMS retrieval.</p>
<h3 id="development-time-eval-harness-to-allow-coding-agent-to-better-help">Development-time eval harness to allow coding agent to better help</h3>
<p>If I had enough resources: curate a set of example inputs and outputs, get a comparator to watch the actual output vs expected output. The comparator can be a function if there is a definitive aspect of the criteria, otherwise an LLM judge that shares an expert's taste. Human will drive the cycle, trigger a batch run, get comparison, after analyzing failure modes, update the prompt (or LLM judge), keep going.</p>
<p>However, for a hobby project, I need to find some shortcut. Inspired by the famous <a href="https://ghuntley.com/loop/">ralph loop</a>, if you can give clear criteria for the coding agent, most likely it will get it for you EVENTUALLY.
Here is what I did:</p>
<ul>
<li>Acquired a deep research report on what a good resume should look like</li>
<li>Fed that into Claude Code, asking for a draft of grader agent that uses scoring rubric (I had some luck with the scoring rubric idea, got some good results in a <a href="https://www.youtube.com/watch?v=dleo77BuB1w">finetuning LLM</a> for wine tasting note generation project)</li>
<li>Test drove the judge on a handful of examples (the draft, the job posting, and the
original resume)</li>
<li>Set up eval harness whose output went directly to the repo</li>
<li>Gave ralph loop instruction: tune the drafting prompt, ensure score from LLM judge needs to be above 88. Then let the ralph do whatever it thinks it should do to reach there (except for changing the judge, it needs my permission to do so if it really thinks judge should change).</li>
</ul>
<p>The drafting agent started to behave a lot better than before, but is it there already? Not yet, but it can sometimes generate something I did not think of.</p>
<h3 id="memory">Memory</h3>
<p>Well well well, I wanted to build it very much! Stay tuned! I am trying to integrate it with another fun project. In this resume project, I got hesitant because of the PII, therefore I removed all the persistence.
Btw, inspired by some tweets, I added <code>CONTINUITY.md</code> as an external memory for the coding agent to know what I have asked it to do. I find it quite helpful even for myself — when vibe coding on a project on and off, it is so easy to lose track of where things are.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why I built TypeCraft</title>
      <link>https://www.hippogriff.io/blog/why-i-built-typecraft</link>
      <guid isPermaLink="true">https://www.hippogriff.io/blog/why-i-built-typecraft</guid>
      <pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate>
      <description>Daily grind becomes daily play</description>
      <content:encoded><![CDATA[<h2 id="the-problem-i-had">The problem I had</h2>
<p>I upgraded to an ergonomic split keyboard lately, and my speed tanked. I could get by leveraging my piano skills, now those compensation patterns stop working - split layout turns out to be too big of a gap to fill.</p>
<p>I have drastically cut back on casual playtime so that I can have more time to learn and build, adding uptight typing drills tips the balance too far - it takes the joy out of the process.</p>
<p>Why not make it fun by building something I like, so daily grind becomes daily play?</p>
<h2 id="space-invaders-but-educational">Space Invaders, but educational</h2>
<h3 id="why-space-invaders">Why Space Invaders</h3>
<p>I am a big fan of an pseudonymous street artist Invader <a href="https://www.space-invaders.com/about/">space-invaders.com</a>, scanning his work during my trips have always been my highlights.</p>
<p>While I was holding my phone getting ready to scan, my eyes are busy taking in every little detail of the streets, hoping to find the hidden-in-plain-sight mosaic art.</p>
<p>I have so many fond memories because of his art.</p>
<h3 id="how-the-game-works">How the game works</h3>
<p>TypeCraft drops letters from the sky as pixel invaders. Type the letter to destroy it. Miss it, and it lands and eats the grape in the center.</p>
<p>The game tracks which letters you miss, which ones take longest, and adapts — sending more of your weak letters as you play.</p>
<p>It's about precision under pressure.</p>
<p>(for the record, I do NOT want to destroy the invaders at all, I just like the feeling of seeing A LOT OF space invaders)</p>
<h2 id="what-i-learned-building-it">What I learned building it</h2>
<p>SPEC SPEC SPEC
It started from a simple idea, but to make it cohesive, it took 45 min iterating with Claude Code to write a spec.</p>
<p>...AND there are still details that both me and Claude missed, like accepting enter key as signal for 'Continue' button.</p>
<h2 id="try-it">Try it</h2>
<p><a href="https://typecraft.hippogriff.io">TypeCraft</a> is live. Play a round, find your weak keys, and let me know what you think.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>