Inspiration

Every immigration tool asks you to pick a country first. That is backwards, and it quietly puts the hardest research back on the person who came for help. You cannot choose Canada for being short of welders if you did not already know Canada was short of welders. The other half is worse. The advice people actually get is a blog post from 2023, a WhatsApp group, or an agent taking a fee. Those are photographs. They were true the day they were written. Then the salary floor moves on a Tuesday with no announcement and somebody has built a year around a number that no longer exists. I wanted the boring version of this. Read the government's own page. Quote it. Date it. Read it again tomorrow.

What it does

You say whether you are trying to study or work. You upload whatever you already hold, a transcript, a passport, a CV, and a phone photo is fine because that is how most people keep their documents. Nobody picks a country. The countries come out of your own papers. A work country appears only when a published shortage list matches something your CV actually says. A study country appears only when a school on that country's official register teaches your level and your subject. Then it builds the guide. Every requirement carries the quote it came from, the page it came from, and the date that page was read. If it cannot find a sentence, it does not make one. The claim goes to an open questions section that says plainly that we do not know yet. Today that guide holds 2,358 requirements across 1,229 government pages under watch. The part that matters is what happens after. A job runs every morning, re-reads all of it, and when a rule moves you get told against your own requirement, with both versions and both dates from the government's own published history. Not a forecast. Nobody needs a model's opinion about what a ministry might do next.

How we built it

One Cloud Run service serves every screen. One Cloud Run job does the reading, ten tasks, five at a time, with the task index picking the lane. Four Cloud Scheduler jobs start it in an order that matters: read the pages at 04:40, ask the boards at 05:00, then tell people at 05:20. Run the digest first and it reports on yesterday. Four service accounts, and the interesting part is what each one cannot do. The researcher reads the web and calls the model and cannot write a row. The writer writes and never touches the network. The watcher is the only thing allowed to append to the snapshot archive and it cannot rewrite it. The web service cannot mint a watcher token, so a request cannot start a crawl. That is not a policy document. tools/test_isolation.py tries every one of those impersonations and a pass means they failed. Gemini 3.5 Flash on Vertex does the reading, through one caller in migragent/model.py so there is one place that retries and one place that logs. Google's ADK runs the four agents. Cloud Vision does OCR on photographed documents, deliberately a different engine from the one making the claims, because using the model to transcribe and then checking the model against its own transcription is marking your own homework. The quote check is the whole thing. A model proposes a requirement and a span it says is on the page. The span is normalised and looked for in the page text. Not there, no row. A real sentence with one number changed, no row.

Challenges we ran into

The one that cost the most was silent. I use a custom BaseLlm so every call goes through one place, and ADK splits an LlmRequest.config into two halves before it reaches you. Six fields are for the client and everything else goes in the request body. Get the split wrong and your agent has no tools, no error, and it answers from memory like a student who did not do the reading. That cost a day. It is filed as google/adk-python issue #6880 with an offline reproduction, and PR #6935 adds the mapping so nobody else has to work it out by watching it fail. Then the whole live site 500ed for days and CI never caught it, because the deploy check only curled /health, which is a static string. A byte-identical revision that served fine on Sunday started failing on Wednesday: a Firestore backend rollout began rejecting a double-encoded (default) database id that the client had always sent. The deploy now curls a route that reads the store. And the honest one: I built sixteen agents and then deleted twelve of them.

Accomplishments that we're proud of

Deleting the twelve. An agent here means the model decides across several steps what to do next, and only four things in this system actually do that. The lane check was one of them until I measured it: a full agent session per page, an hour for six pages, which makes a 143 page round take most of a day. It is one model call now with the check in code. Same judgment, no session. The architecture doc names all twelve and says why each one is not an agent. Then: the quote check refusing things. It is easy to build something that says a lot. Every requirement in there survived being asked to point at the sentence. And it is genuinely live and genuinely autonomous. Four scheduled rounds ran this morning while I was asleep, and the guide today is not the guide from last week.

What we learned

Counting agents is the wrong instinct. The pull is to add one for every job because it looks like architecture, and every one you add is a session, a latency budget and a place to be wrong. The useful question is whether the model needs more than one step to decide, and most of the time it does not. Also that a test which only proves a denial proves nothing. Everything denies when the database is unreachable. test_isolation checks that the writer CAN write in the same run, and that pair is what makes the denial evidence.

What's next for Migragent

Turn on the second reader in production. A Gemma 4 pass already re-reads what Gemini extracted and disagrees out loud, and it ships switched off because turning everything on the week of judging would contradict the restraint the whole thing is built on. Same for the lane check and the agent extractor: on, lane by lane, reading the pages they reject rather than the rate they reject at. More boards. The work path only offers a country when there is a government job board we can actually read, which today is Canada. The UK's will not serve robots.txt and Spain's did not answer. Being told we cannot help is worse news and better information than being sent somewhere we cannot follow through on. Then licensing bodies, because for nurses and electricians the visa is the easy half.

Share this project:

Updates