Inspiration

Google’s ADK resume docs — and this hackathon’s 13 Aug webinar — warn that a resumable agent might order two laptops. Resume is at-least-once: tools “are run at least once, and may run more than once.” If that tool is a purchase, the shop charges twice.

That is not a chatbot problem. It is a tool-level lock. We built Double-Act Guard as a Taskmaster for that one chore: one human approval, one Pixel Laptop, still one order after crash and retry.

What it does

A human approves a single pixel-laptop purchase. Gemini 3.5 + Google ADK calls place_order. We stop after the shop writes the order and before ADK records the tool result — the gap where resume re-runs an incomplete tool.

Mode After crash + resume
Unguarded 2 receipts — the trap
Guarded 1 receipt — Firestore writes the order and the idempotency key in one transaction; resume returns skipped_duplicate

Judges do not have to trust a screenshot. /proof/{run_id} recounts orders from the store and shows pass/fail.

Live: Cloud Run · repo

This is a demo shop (one SKU). It is not a real charge. The product is the lock, not a storefront.

How we built it

  • Gemini 3.5 Flash via the Gemini API
  • Google ADK (App + ResumabilityConfig(is_resumable=True)) so the first pass is a real agent tool call
  • FastAPI dashboard: approve, crash-after-commit switch, resume, tool log
  • Cloud Run (europe-west1) so judges get a public URL
  • Firestore Native: double_act_orders + double_act_idempotency in one transaction on the guarded path

Unguarded inserts an order only. Guarded inserts order + key together. Resume always re-invokes place_order. The proof page recomputes order_count from the shop, not from the UI.

Challenges we ran into

  • Cloud Run + Developer Connect could not fetch GitHub (fetchReadToken). We deployed with gcloud run deploy --source instead of fighting IAM overnight.
  • The first live Approve failed because Cloud Firestore API was disabled and writes used Python enums Firestore cannot store. We enabled Native Firestore, granted roles/datastore.user, and JSON-dumped documents.
  • Gemini can pass guarded=true as a tool argument even in unguarded mode. For the guarded demo we keep ADK off on that pass so the mode cannot be overridden. The metric still has to match the proof URL.

Accomplishments that we're proud of

  • A judge can reproduce 2 vs 1 on a public URL without our laptop.
  • The sponsor kit is the feature: ADK resume + Firestore transaction, not a chat wrapper.
  • Claims match the live app: unguarded → 2, guarded → 1.

What we learned

ADK resume is not a Google bug. It is at-least-once delivery. If you write the order first and the key after, you still double-charge. The product is one transaction: order + idempotency key. A screenshot of “it worked on my machine” is not a metric.

What's next for Double-Act Guard

Same lock on a real irreversible tool (Calendar invite, pay API) where a duplicate actually hurts. Keep the public proof URL. Do not hide the unguarded trap — it is the teaching case.

Created for the purposes of entering the All Things Agentic Hackathon.

Built With

Share this project:

Updates

Submission history