Inspiration
Looking after someone at home is an operations job nobody trained you for.
The care itself is not the hard part. The hard part is everything around it: eight medications with overlapping schedules, appointments at three different providers, a benefits deadline buried in paragraph three of a letter that opens with two paragraphs saying nothing needs doing. Lab results that arrive as a crooked photo of a printed page. A note from the district nurse recorded as a voice memo in a corridor. It runs for months, the inputs are genuinely bad, and the cost of missing one thing is real.
Almost every agent demo I have seen is built for somebody at a desk — a PM, a developer, a sales rep. I wanted to build for the person the software industry never builds for, and to find out what an agent fleet has to be like before you would trust it near a parent's medication list.
What it does
Vigil is five agents that read the paperwork, act on it, and stop before anything that cannot be undone.
- It reads what actually arrives. Photographs, voice notes, scans, PDFs — with a confidence and a source on every single claim. A printed drug name scores 0.98. A dose in a voice memo where the carer said "I think? I should check that" scores 0.50, and that number is what decides whether a human is interrupted.
- It keeps working when nobody opens the app. A deadline read out of a letter is recorded the moment it is found and chased on a fixed ladder every morning until somebody discharges it — day 14, day 7, day 3, due today. That loop is the only thing in the system that starts because a person did nothing.
- It shows the instruction rather than restating it. A discharge letter says to rest the leg on a cushion so the ankle sits higher than the hip. That is correct, a clinician wrote it, and it is delivered on the worst medium available — a sheet of A4 handed over once at a hospital desk. Vigil asks to film it, a person approves, and Veo renders the movement. Vigil writes none of the words: the clinician's sentence is quoted beside the clip, and the numbers stay as text because a generated picture cannot be trusted with a figure.
- It refuses, out loud. Every clinical action goes to a human at any
confidence. Every refusal is recorded with the id of the rule that refused it —
deny.unscopedis something an auditor can argue with; "denied" is not.
The twist: an agent that gamed its own evaluation, and was caught
The part I am proudest of is a rejection.
Agents in Vigil can propose rewrites of their own instructions. A proposal is scored against a fixed golden set, and a second model reads the diff as well as the score.
One proposal scored 0.75 → 0.83. Every simple check said promote it. It was
rejected, because the diff had hardcoded the drug names from the test suite —
If asked about Metaform, state that Metaform is not in the medication graph.
That is memorisation, not capability, and a gate that only reads the number
cannot tell the difference.
Both records are kept in the registry: one rejection for gaming, one for ordinary quality. A gate that has only ever said no is indistinguishable from a gate wired to say no.
How I built it
Google ADK for the fleet, Gemini 3.5 and 3.6 on Vertex AI for the reasoning tier, and Cloud Run scaled to zero so an idle week costs nothing.
The architecture is five agents with per-agent identity rather than per-agent instructions. Each agent's registry entry lists the scopes it holds, and that is enforced twice: at assembly, where an agent is never handed a tool it does not hold, and at call time, where a call is refused and audited even if a tool reaches it by some future mistake. The benefits agent does not have a clinical tool in its context to call — which is a different thing from being asked not to call one.
Everything that touches the world goes through a single action gate: idempotency claim, then the policy engine, then execution, then completion, with compensating actions unwound in reverse on failure. The claim is taken before the side effect and completed after, so a worker killed mid-flight resumes without doing anything twice.
Gemma runs the PII redaction pass before anything reaches the reasoning tier — the pass a regex cannot do, because a person's name is the identifier that matters most and the one no pattern finds. Gemini TTS reads the weekly summary aloud, for the hours when a carer's hands are full and a screen is the one thing they cannot look at.
Challenges I ran into
A good fallback hides the failure it was built for. The UI fell back to committed sample data whenever the API failed, and labelled itself honestly. That badge said "sample data" for the entire life of the deployment and I never noticed, because I was testing the API with curl and it answered every time. Build env vars do not reach a Dockerfile as build args, so the key was empty in every image ever shipped. Three separate bugs were hiding behind that one fallback, and all three surfaced within an hour of deleting it.
A tool signature is a form the model fills in. Five tools took run_id: str.
The framework builds tool declarations from signatures, so the model supplied
it — with "run-001", a plausible id belonging to no run that has ever existed.
Every approval created that way was untraceable, and the idempotency key was
namespaced under the hallucinated id. Bookkeeping the model cannot know is
bookkeeping the model will invent.
A branch nobody could reach. The board's central claim — "this clears lunchtime and creates an interaction the record warns about. Both are true; neither cancels the other out" — only renders when a move both relieves crowding and introduces an interaction. The corpus had exactly one crowded slot, so no move could ever reduce the crowded count. The sentence the whole product argues for was unreachable against the only data anyone runs it on.
The failure that looks like success. A .wav written without a RIFF header
plays nowhere and shows 0:00 / 0:00 — indistinguishable from a file that has not
downloaded yet. It reached a screenshot before it reached a test.
What I learned
Refusing to help is not the same as being safe.
The shared rule every agent read used to say "you do not give medical advice". It was tidy, easy to defend, and it meant the part of the folder a family most struggles with was the part the fleet would not touch — while it went on cheerfully filing their insurance paperwork. The rule is now precise instead of broad: carrying a clinician's words is allowed, authoring is not. The safety never came from the silence. It comes from the quotation, the source, and a named human at the gate.
The same lesson showed up in the policy engine, which had five ways to say no and
one way to say yes called allow.default — the thing left over once every refusal
had been enumerated. In a system whose whole argument is restraint, that meant
nobody had ever written down what the fleet is for.
What is next
Semantic recall. memory.recall() currently matches a field name by exact string
equality, so asking about "sleep" when the claim is stored as sleep_quality
returns nothing — the same failure class as a discovery bug that once sent an
agent round a 40-call loop. It is the obvious next thing this store should do, and
it is honestly recorded in ADR 004 as a reason for choosing Firestore that has not
been collected yet.
Built With
- cloud-run
- cloud-scheduler
- cloud-storage
- cloud-trace
- docker
- fastapi
- firestore
- gemini
- gemini-tts
- gemma
- google-adk
- google-cloud
- next.js
- opentelemetry
- pub/sub
- pydantic
- python
- react
- tailwindcss
- typescript
- veo
- vertex-ai
Log in or sign up for Devpost to join the conversation.