<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Donny Zimmerman</title>
    <description>The latest articles on DEV Community by Donny Zimmerman (@dzim89).</description>
    <link>https://dev.to/dzim89</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4102760%2F68dfd00b-cd33-4c81-8123-9c052c6f043f.png</url>
      <title>DEV Community: Donny Zimmerman</title>
      <link>https://dev.to/dzim89</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dzim89"/>
    <language>en</language>
    <item>
      <title>Building Hearthbeat in One Night: A Safe Household Agent with Google ADK</title>
      <dc:creator>Donny Zimmerman</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:02:44 +0000</pubDate>
      <link>https://dev.to/dzim89/building-hearthbeat-in-one-night-a-safe-household-agent-with-google-adk-2an8</link>
      <guid>https://dev.to/dzim89/building-hearthbeat-in-one-night-a-safe-household-agent-with-google-adk-2an8</guid>
      <description>&lt;p&gt;&lt;em&gt;This article was created for the purposes of entering the All Things Agentic&lt;br&gt;
Hackathon. Category: The Taskmaster.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I built an autonomous household-operations agent in one night with Google's&lt;br&gt;
Agent Development Kit, Gemini 3.5 on Vertex, and a stack of Google Cloud&lt;br&gt;
plumbing — and the most interesting bugs were the ones my own privacy guard&lt;br&gt;
caught. Build log below, roughly in commit order.&lt;/p&gt;

&lt;p&gt;The motivation was painfully ordinary: a school email gets buried, the kids do&lt;br&gt;
not know what to prepare on their own, and an after-school change surfaces only&lt;br&gt;
when we reach drop-off. One rough morning also revealed that we had run out of&lt;br&gt;
easy breakfast food. I did not claim a one-night MVP could solve all of that. I&lt;br&gt;
started with the repeatable failure at the center — school-email and calendar&lt;br&gt;
facts reaching us too late — and built a dependable scheduled agent around it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The product in one sentence
&lt;/h2&gt;

&lt;p&gt;No chat UI: a Cloud Scheduler cron fires an ADK pipeline on Cloud Run that&lt;br&gt;
reads a token-scrubbed mirror of my actual home (Home Assistant, family&lt;br&gt;
calendar, school email), plans the day, refuses its own bad ideas against a&lt;br&gt;
default-deny whitelist, asks my phone for a permission slip before releasing a&lt;br&gt;
person-facing draft, and then my house &lt;em&gt;pulls&lt;/em&gt; approved actions—the cloud&lt;br&gt;
service does not initiate connections into the home.&lt;/p&gt;
&lt;h2&gt;
  
  
  Hour 0: kill the scary unknowns first
&lt;/h2&gt;

&lt;p&gt;Two probes before any code: (1) which Gemini 3.5 IDs actually answer in my&lt;br&gt;
project — answer: &lt;code&gt;gemini-3.5-flash&lt;/code&gt; and &lt;code&gt;-flash-lite&lt;/code&gt;, &lt;strong&gt;only at&lt;br&gt;
&lt;code&gt;location=global&lt;/code&gt;&lt;/strong&gt; (us-central1 404s all of them); (2) the empty-text trap —&lt;br&gt;
with a small &lt;code&gt;maxOutputTokens&lt;/code&gt;, thinking consumes the whole budget and&lt;br&gt;
&lt;code&gt;.text&lt;/code&gt; comes back empty. Both went straight into a canary script&lt;br&gt;
(&lt;code&gt;infra/canary.py&lt;/code&gt;) and hard rules in the model factory: floor&lt;br&gt;
&lt;code&gt;max_output_tokens&lt;/code&gt; at 2048, always set an explicit &lt;code&gt;ThinkingConfig&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The ADK composition that survived contact with the API
&lt;/h2&gt;

&lt;p&gt;Original sketch: "the planner drafts the plan and calls tools." Real API:&lt;br&gt;
&lt;strong&gt;&lt;code&gt;output_schema&lt;/code&gt; disables tools.&lt;/strong&gt; So the shape became four tool-bearing&lt;br&gt;
gatherers (&lt;code&gt;ParallelAgent&lt;/code&gt;, distinct &lt;code&gt;output_key&lt;/code&gt;s — the docs warn you about&lt;br&gt;
state races) feeding a tool-less structured planner, then a &lt;code&gt;LoopAgent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PolicyGate (custom BaseAgent, pure code) → critic (LLM) → reviser (LLM), ≤3 turns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PolicyGate is the fun part. It runs a deterministic whitelist/quiet-hours/&lt;br&gt;
budget check, hashes the plan, and only escalates (ends the loop) when the&lt;br&gt;
findings are empty AND the critic's structured &lt;code&gt;Critique&lt;/code&gt; both passes and&lt;br&gt;
&lt;strong&gt;echoes the hash of the exact plan revision it graded&lt;/strong&gt;. A stale "pass" can&lt;br&gt;
never green-light a newer plan. The dispatcher then re-checks every action&lt;br&gt;
anyway — and the house-side poller checks a third time before touching Home&lt;br&gt;
Assistant. Default-deny, enforced three times, all from one YAML file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy that doesn't depend on a model behaving
&lt;/h2&gt;

&lt;p&gt;On the intended house-to-cloud path, mirrored and ingested free text goes&lt;br&gt;
through &lt;code&gt;deep_scrub&lt;/code&gt;: deterministic token map → &lt;strong&gt;local Gemma 3 via ollama&lt;/strong&gt;&lt;br&gt;
(a local Gemma/Qwen tier scans for additional PII the family map can't know —&lt;br&gt;
the teacher's name and phone in a school email) → token map again → scrub&lt;br&gt;
validation. My 4080 is so VRAM-contended by other local&lt;br&gt;
models that gemma runs mostly on CPU at ~12 tok/s — fine for short scans, and&lt;br&gt;
the ledger records which tier did each pass (&lt;code&gt;PRIVACY_TIER=qwen&lt;/code&gt; is the&lt;br&gt;
fallback).&lt;/p&gt;

&lt;p&gt;Cloud-side, an ADK &lt;code&gt;BasePlugin&lt;/code&gt;'s &lt;code&gt;before_model_callback&lt;/code&gt; scans every instrumented outbound&lt;br&gt;
Vertex request against &lt;strong&gt;salted hashes&lt;/strong&gt; of the protected names. The guard&lt;br&gt;
stores and compares salted alias hashes.&lt;/p&gt;

&lt;p&gt;Three real bugs this architecture caught &lt;em&gt;during the build&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;My token for Dad was &lt;code&gt;[[P_DAD]]&lt;/code&gt; — and the alias regex happily matched
"DAD" inside the token, producing &lt;code&gt;[[P_[[P_DAD]]]]&lt;/code&gt;. Scrub only outside
token spans.&lt;/li&gt;
&lt;li&gt;My "hash every word of multi-word aliases" idea hashed the word &lt;strong&gt;"the"&lt;/strong&gt;
(from a room alias) — instant false positives on all English text.
Per-word hashes now apply only to person names, ≥4 chars, minus a
family-word stoplist.&lt;/li&gt;
&lt;li&gt;The big one: I ran the first mirror with the fixture map instead of the
real one, so real first names landed in Firestore. On the very next cloud
run, &lt;strong&gt;the egress guard refused to call Gemini&lt;/strong&gt; — 2 hash matches,
blocked, run failed. The privacy system's first real catch was my own
mistake. It's in the README's honesty ledger.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Run integrity as a feature
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;POST /run&lt;/code&gt; authenticates the configured invoker principal via OIDC; for the&lt;br&gt;
filmed run, Scheduler history and fire-time logs corroborate cron origin.&lt;br&gt;
The filming/judge endpoint hard-codes &lt;code&gt;manual&lt;/code&gt;. When you see "scheduled" in the&lt;br&gt;
filmed demo, it is corroborated by Scheduler history.&lt;/p&gt;

&lt;p&gt;Also: GFE quietly intercepts the literal path &lt;code&gt;/healthz&lt;/code&gt; on &lt;code&gt;*.run.app&lt;/code&gt; and&lt;br&gt;
serves a Google 404 that never reaches your container. Renamed to &lt;code&gt;/health&lt;/code&gt;.&lt;br&gt;
You're welcome, future me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Judge mode
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SIMULATED_HOME=1 docker compose up&lt;/code&gt; from a clean clone: the same container&lt;br&gt;
image, the real &lt;strong&gt;Firestore emulator&lt;/strong&gt; (real transactions and &lt;code&gt;create()&lt;/code&gt;&lt;br&gt;
preconditions), a fake Home Assistant seeded from fixtures, recorded Gemini&lt;br&gt;
responses, and a kickoff that fires &lt;code&gt;/trigger&lt;/code&gt;. Substitutions only where no&lt;br&gt;
free emulator exists (BigQuery → JSONL), each disclosed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Per-run cost: a run-scoped list-rate estimate at official configured
rates (&lt;code&gt;runs_v.cost_cents&lt;/code&gt; in BigQuery); a configured observed-spend
threshold makes the policy layer deny the plan (not a hard billing cap).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;egress_violations_v&lt;/code&gt;: zero protected-alias matches in the filmed run.
Historical blocked rows are shown separately as caught build failures.&lt;/li&gt;
&lt;li&gt;Unit tests: the scrub round-trip and policy table tests are the graded core.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/DZim89/hearthbeat" rel="noopener noreferrer"&gt;https://github.com/DZim89/hearthbeat&lt;/a&gt; · Mission Control (live,&lt;br&gt;
read-only, token-space): &lt;a href="https://hearthbeat-369944070051.us-central1.run.app/missioncontrol" rel="noopener noreferrer"&gt;https://hearthbeat-369944070051.us-central1.run.app/missioncontrol&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I built Hearthbeat solo with my home-grown AI Agent Fleet Workspace: a&lt;br&gt;
private multi-agent engineering environment that gives Claude Code,&lt;br&gt;
Antigravity/Gemini, Codex, and geminiclaw a shared plan, bounded work, and&lt;br&gt;
evidence gates under my direction. The first repository commit is timestamped&lt;br&gt;
August 30, 2026 at 4:07:10 PM PDT. My house now holds a better morning standup&lt;br&gt;
than most teams I've worked on.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AllThingsAgenticHackathon
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>gemini</category>
      <category>googlecloud</category>
    </item>
  </channel>
</rss>
