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.
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_idempotencyin 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 withgcloud run deploy --sourceinstead 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=trueas 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
- cloud-run
- css
- fastapi
- firestore
- gemini
- gemini-3.5-flash
- google-adk
- google-cloud
- html
- javascript
- pydantic
- python
- secret-manager
- uvicorn
Log in or sign up for Devpost to join the conversation.