What Revoyé is
A retail supply chain agent swarm that scans live inventory on a schedule, decides what to reorder, and raises the purchase orders on its own, behind guardrails that stop it acting on poisoned supplier email.
Live at https://revoye-900638801452.us-central1.run.app
Inspiration
A mid-sized retailer usually has one person watching which products are running low, emailing suppliers, comparing what was quoted last time, drafting purchase orders, and chasing shipments. The most valuable part of that job, remembering that one supplier was late twice and another came back with better terms, lives only in their head. I wanted to put it somewhere durable and let a swarm act on it without being asked.
What it does
Unattended, on a schedule. A Cloud Run Job wakes on a timer, reads Bellworth Home's live inventory from Firestore, and hands every SKU at risk of stockout to the swarm. Nobody types anything. A real run:
Scanned 5 SKUs, 3 at risk → SKU-9902 (critical) via Northwind Ceramics blocked → SKU-3390 (critical) via Kettleworth Foundry ordered → SKU-4471 (high) via Kettleworth Foundry ordered
Run complete in 48s {'blocked': 1, 'ordered': 2} Needs a human: SKU-9902
Two purchase orders raised and written to Firestore with real references and values. One supplier refused, because its inbound correspondence failed security screening. One line for a person to look at, instead of a 2,400-row catalogue.
Interactively, when asked. The same swarm answers direct questions through the deployed web interface, with a full reasoning trace behind every decision.
An orchestrator receives a goal and delegates to three specialists using ADK's transfer_to_agent:
- demand_forecaster reads live stock and flags stockout risk from weeks of cover against the reorder point
- supplier_negotiator reads inbound correspondence, recalls prior terms from memory, and raises purchase orders
- logistics_tracker tracks shipment status and reads carrier correspondence
How I built it
Google ADK 2.6.2 with Gemini 3.6 Flash on Vertex AI across all four agents. The governance layer is built entirely on ADK's callback surface:
| Component | Hook | What it does |
|---|---|---|
| Model Armor | after_tool_callback |
Two-pass injection screening (patterns, then a model classifier) plus PII redaction on untrusted inbound messages |
| Agent Gateway | before_tool_callback |
Blocks purchase orders from quarantined suppliers or above a value threshold |
| Agent Identity | before_tool_callback |
Per-agent tool and data scoping, default-deny for anything unregistered |
| Memory Bank | Firestore-backed tools | Negotiation history that survives sessions, processes and container restarts |
| Resilience | on_model_error_callback |
Retries with backoff on 429/503, then degrades to an explicit no-action response |
Five Google Cloud services carry it: Vertex AI for the model, Cloud Run for the deployed swarm and its web interface, Cloud Run Jobs for the autonomous pass, Cloud Scheduler for the timer that fires it, and Firestore for state.
Four Firestore collections hold that state: inventory (the product master), supplier_memory (agreed terms and reliability notes the agents write), purchase_orders (POs the swarm has raised, with reference and value), and runs (one document per autonomous pass, with the tally and the exception list).
Challenges I ran into
A delegating agent's turn ends early, so its callbacks never fire. The Memory Bank silently recalled nothing for a long time. load_memory ran without error and always came back empty. The save callback sat on the orchestrator, but when ADK hands off via transfer_to_agent it ends the orchestrator's turn early, so its after_agent_callback never runs on a delegated turn. Only the agent that finishes the turn gets its callback invoked. The parameter must also be named callback_context, since ADK calls it by keyword and a mismatched name fails quietly.
Guardrails at the model boundary do not work. My first Model Armor screened llm_request.contents in a before_model_callback, which sounds like the safest possible place. It failed in both directions at once: the injected message passed straight through, because it arrived as a tool result rather than as conversation history, while a benign message got blocked because Armor matched its own earlier quarantine notice sitting in the history. Screening at the model boundary cannot distinguish attacker-controlled text from the agent's own trusted output.
Patterns only catch the attacks you already imagined. Screening started as regular expressions, which caught "ignore all previous instructions" and nothing phrased any other way. A polite note claiming a revised supply agreement, asking the agent to treat an account as pre-approved and not raise it with operations, went straight through. Screening now runs two passes: patterns first because they are free, then a model classifier for anything they clear. The classifier fails toward the pattern verdict rather than blocking everything, because a guardrail that breaks open under load is not a guardrail.
Default-deny needs a complete allow-list. Agent Identity initially blocked the orchestrator's own transfer_to_agent. Delegation failed silently, and rather than reporting that it could not act, the model wrote a convincing purchase order in prose. My own guardrail caused a hallucinated order.
The dev harness hides its dependencies. Running the swarm outside adk web for the scheduled job surfaced two implicit dependencies in a row: the .env file is loaded by the ADK CLI but not by a bare Python process, and Runner needs an explicit memory_service or the memory callback raises. Neither is visible until you leave the harness.
Accomplishments that I am very proud of
Moving the screen to after_tool_callback, where untrusted data actually enters the system, fixed both Armor failures. The same poisoned message that previously produced a 100-unit purchase order now produces a quarantine notice and an escalation.
More importantly, I learned that persuading a model is not the same as constraining it. Even with the message quarantined, the model still attempted the draft. The quarantine text told it not to; it tried anyway. So enforcement moved into before_tool_callback, where returning a value means the tool never executes at all. The trace from that run is the clearest artifact in the project: the model requesting the draft, and the tool never running.
And the autonomous pass works end to end. The swarm detects risk, screens hostile input, enforces policy, raises real orders, and records what it agreed, on a schedule, with nobody watching.
What I learned
Retry with backoff started as a workaround for testing and turned out to be the resilience story the track was asking for. The failure mode was the feature.
I also settled on a rule for degradation: a fallback must never resemble a normal answer. In a procurement system, a plausible-looking response is how you get a phantom order nobody placed, so error_code and error_message are always populated and the body states plainly that no purchase order was drafted and no record was written. The same rule shaped the autonomous runs, where outcomes are classified from tool results rather than from the model's prose, so a run summary reflects what actually happened rather than what the agent claimed.
The broader lesson is about where a guardrail sits. Detection methods are swappable — I replaced regex with a classifier in an afternoon. What made that cheap was that enforcement lives below the model, at the tool boundary, so improving how an attack is spotted never required changing what happens once it is.
What's next for Revoyé
- A human approval queue. Orders sitting at
HELD_FOR_APPROVALand suppliers flagged by Armor currently land in Firestore with nowhere for an operator to act on them. That interface is the obvious next build - Real connectors: an ERP inventory sync feeding the product master, and a procurement system that receives the orders, behind the same guardrails
- Widening PII redaction, which is still pattern-only, beyond the contact formats it was written for
- Agent Registry so the fleet is discoverable across departments
- Delivery tracking against a real logistics provider rather than a stub
Built With
- cloud-run
- gemini
- google-adk
- google-cloud
- multi-agent
- python
- vertex-ai

Log in or sign up for Devpost to join the conversation.