Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Chapter 12: Deploy Your Scout (Production Lab)

My first real run of this scout crashed halfway through:

google.genai.errors.ClientError: 429 RESOURCE_EXHAUSTED.
Quota exceeded for metric: generate_content_free_tier_requests,
limit: 20, model: gemini-3.5-flash

Twenty model calls a day, on the free tier, and my design needed about fifty. None of the eleven chapters before this one could have shown me that, because they all ran on made-up papers and a stand-in model. That was deliberate: it kept every failure reproducible. It also means the scout had never met a real paper.

This chapter is where it does. It’s longer than the others and closer to a small software project, so I’ve split it into seven stages: replace the simulation, make it reliable, test it, deploy it, observe it, attack it, and finally open it up so other readers can change it. Along the way I’ll show you what broke when I built it, and what I changed.

It’s the one chapter that runs in Colab from start to finish, because it needs the internet and an API key, and this page can’t provide either.

Chapter 12 of 12 · Production lab

Where we left off: The scout’s whole lifecycle works, but only on made-up papers and a stand-in model.

What we fix now: Build the real scout on live arXiv papers and Gemini, make it survive real limits, test it, deploy it on a schedule, and measure it on real use.

By the end, you can:

  • run a small agent on real data within a fixed daily budget of model calls

  • tell software tests and AI evals apart, and write both

  • deploy an agent on a schedule, keep its key out of the code, and measure its cost

What changes, and what doesn’t

Chapters 1 to 11This chapter
Papersmade up, written into the notebookthe newest papers from arXiv’s free API
Modela hand-written stand-inGemini, with your API key
Where it runswhen you press a buttonGitHub Actions, every weekday
Where results goprinted below a cellan issue in your GitHub repo, plus logs

The design is v1.2 from Chapter 10 at the “suggest” setting: tools (Chapter 3), a scope with no publish action anywhere (Chapter 5), the full-text step done by code (Chapter 9), the guideline (Chapters 4 and 9), memory (Chapter 9), the grounding rule and check (Chapters 8 to 10), and a log line for every paper (Chapter 5). Two things are new, and real use forced both on me: the model works on batches of papers, and it replies in a schema instead of free-form JSON.

Stage 1: replace the simulation

Your key

Get a free Gemini key at aistudio.google.com/apikey. This time it doesn’t go into the code. Colab has a place for it:

  1. Click the key icon in Colab’s left sidebar (Secrets).

  2. Click Add new secret, name it GEMINI_API_KEY, and paste your key as the value.

  3. Switch on Notebook access for it.

The key never appears in the notebook, so you can share the notebook without leaking it. That matters more now, because in Stage 4 this code goes into a GitHub repo, and a repo’s history keeps everything ever committed to it.

One limit to know about before you start: the free tier allows only a small number of model calls per day, per model, and it varies by account. Mine was 20 for gemini-3.5-flash. Check yours at aistudio.google.com/rate-limit. Your Colab tests and your deployed scout share that allowance, because they use the same key.

Settings

config.py holds everything you’d want to change: your interest, the guideline, the arXiv categories, the model, the threshold, and the call budget. Change INTEREST and GUIDELINE to your own topic if you like; nothing else in the code cares what the topic is.

Two settings deserve a note. PROMPT_VERSION is a label I bump every time I change a prompt or the guideline. Every log line records it, so months later I can still tell which prompt produced which verdict. CALL_BUDGET caps the model calls per run, for the reason in the opening of this chapter.

The tools

arxiv_source.py holds the scout’s two tools: fetch_new_papers asks arXiv’s API for the newest papers in your categories, and fetch_full_text reads the start of a paper from arXiv’s HTML version.

I read the HTML version instead of the PDF on purpose. PDFs are large and messy to turn into text, and the first few thousand characters of the HTML usually contain the method, which is all the scout needs. Some papers don’t have an HTML version; then the scout decides from the abstract and logs that it did.

The retries are there because of a real failure. On my second morning, arXiv’s API didn’t answer within 30 seconds and the whole run stopped. Now the paper list gets 90 seconds and four attempts. The time.sleep(3) is arXiv’s own request: automated tools should wait three seconds between requests.

These are real papers, so your list will differ from mine, and from yours tomorrow.

Stage 2: make it reliable

Batches and a schema

The first version of judge.py worked like Chapter 9: two model calls per paper, one to screen and one to decide. That’s where the 429 error came from. With 25 papers a day it needed up to 50 calls, against a limit of 20.

Batching fixed it. screen_prompt lists up to 25 titles and abstracts and asks which could match. verdict_prompt gives the model four plausible papers at a time, each with the start of its full text, and asks for one verdict per paper. A day with 100 new papers now takes about four screening calls and two or three verdict calls.

The second change is the reply format. Since Chapter 2 the scout has asked for JSON and pulled it out with a regex. Here the expected shape is written down as Pydantic models (ScreenReply, VerdictReply), and Gemini is asked to follow that schema directly, which is called structured output. That removes Chapter 2’s chatty-reply problem.

It doesn’t remove the need to check. A schema promises the shape: a list of integers, a confidence that’s a number. It can’t promise that paper 7 exists in a batch of four, or that a reason doesn’t invent a number. So parse_screen and parse_verdicts still check ranges, and make_record still runs the grounding check from Chapter 8. I think of it as two layers of validation: the schema handles the shape, my code handles the meaning.

Both prompts also say that the papers are data, not instructions. Stage 6 tests how much that line is worth.

State, budget and failures

scout.py ties it together. It fetches papers, screens and decides in batches, and writes five files: digest.md (today’s suggestions), log/verdicts.jsonl (one line per paper), log/runs.jsonl (one line per run, with calls, tokens and time), seen.json (memory of papers already decided) and pending.json (papers still waiting).

Most of its code is about bad days, because I had several:

  • Budget counts model calls and stops at CALL_BUDGET.

  • A failed call is retried with growing waits. gemini-3.5-flash answered with ServerError (overloaded) on both of my first two days, and the retries usually got through.

  • A daily-quota error stops the run straight away. Retrying it can’t work until tomorrow.

  • When the scout stops early, the papers it didn’t get to go into pending.json, and the next run starts with them, even if newer papers have pushed them out of arXiv’s newest 100 by then.

  • Every decided paper is written to the log the moment it’s decided.

My first version also fetched only the 25 newest papers. On a normal day that’s a small slice of what’s posted, and the papers it never fetched didn’t appear in any log, so no scorecard could count them as missed. Batching made it cheap enough to fetch 100.

The automatic_function_calling setting turns off a library feature that lets Gemini call Python functions on its own. The scout runs its tools in its own code (Chapter 5), and turning it off also silences a warning the library prints otherwise.

Read the digest the way you read v1’s suggestions in Chapter 5. When I ran it for the first time, it suggested 10 papers out of 100, and five of them had a confidence of exactly 1.00. If almost every suggestion scores near the top, the 0.5 threshold barely filters anything, and the numbers stop saying which suggestions are strongest. I’m watching whether that continues before changing anything.

The same digest showed me a gap in my guideline. Several suggestions were about agent memory: a model retrieving its own stored experiences, not documents. My guideline doesn’t say whether that counts. It’s the Chapter 4 situation exactly: a paper I can’t label consistently means a rule is missing. I haven’t decided yet.

Look at a few skipped papers too. Skipped papers are where missed ones hide:

Run scout.run() a second time. It should report 0 to decide and make no model calls. That’s the memory from Chapter 9. (If the first run stopped early, the second one picks up the waiting papers instead.)

Stage 3: test it

The evaluation in Chapter 6 asked whether the scout’s judgments are good. That’s a different question from whether the software does what I wrote it to do: skip papers it has seen, stop at the budget, keep papers when the quota runs out, reject paper 7 in a batch of four. Those parts are deterministic, so they get ordinary tests, with a fake model that costs nothing and answers the same way every time.

Software testsAI evals
Questiondoes the code follow its specification?are the model’s judgments useful and grounded?
Modela fake, written in the testthe real one
Resultpass or fail, every timea number that moves from run to run
Runsbefore every deploymenton labeled and real data

The tests below are the checks I ran by hand while building this chapter, written down so they run automatically. The deployed workflow runs them before every scout run, and if one fails, the scout doesn’t run.

Stage 4: deploy to GitHub Actions

Running the scout by hand isn’t deploying it. GitHub Actions runs it on a schedule, for free, on GitHub’s machines.

scout.yml runs the tests, then the scout, then posts digest.md as a new issue and commits the logs, memory and waiting papers back to the repo, so the next run continues where this one stopped. Each of these lines is there because something went wrong without it:

  • if: always() on the last two steps. My first crash skipped them, and that run’s work was lost. Now they run even if the scout fails.

  • python -u. Without it, Python holds back printed output when it isn’t writing to a terminal, and the Actions log stayed empty for minutes while the scout was working fine.

  • 17 8 * * 1-5, weekdays at 08:17 UTC. I first scheduled 06:17 UTC, then noticed that’s before Gemini’s daily allowance resets at midnight Pacific time (07:00 or 08:00 UTC, depending on the season). Every scheduled run would have started in the previous day’s used-up window. The odd minute avoids the start of the hour, when many scheduled jobs on GitHub begin.

evaluate.py and evaluate.yml are for Stage 5. requirements.txt lists the scout’s two packages, and .gitignore keeps secrets and the daily digest out of the repo.

The first line clears this Colab run’s logs and memory, so the deployed scout starts fresh. You don’t strictly need the zip, though. The same files live in a shared repo, open-paper-scout, and the quickest way to deploy is to copy that:

  1. Make your copy: open open-paper-scout and click Use this template → Create a new repository, for example paper-scout. Make it private: the issues are your reading list.

  2. Bring your changes: if you edited config.py (or anything else) in Colab, open that file in your new repo, click the pencil icon and paste your version. That’s what the zip is for.

  3. Add your key as a secret: Settings → Secrets and variables → Actions → New repository secret. Name: GEMINI_API_KEY.

  4. Run it once: Actions → Paper scout → Run workflow. After a few minutes, your first digest is in the Issues tab.

If a run sits at “Queued” for a while, GitHub is still finding it a machine; that’s normal. If “Save log and memory” fails with a permission error, go to Settings → Actions → General → Workflow permissions and choose Read and write permissions. Until the secret exists, the scheduled run only leaves a note that the key is missing, so a fresh copy doesn’t fill your inbox with failures.

Stage 5: observe it

This is Chapter 7’s spot-check and Chapter 10’s scorecard, on the real log. After a week of runs:

  1. Actions → Evaluate scout → Run workflow with sample_size set to 15. This adds 15 random papers from the log to labels.csv, including ones the scout skipped.

  2. Open labels.csv on GitHub, click the pencil icon, and fill in each label: 1 relevant, 0 not relevant, using the guideline in config.py. Label from the paper, not from the scout’s verdict.

  3. Run Evaluate scout again with sample_size set to 0. The scorecard appears in the run’s summary.

The scorecard has Chapter 10’s rows, plus what only real use produces: how often a full text was available, which prompt versions are in the log (and a warning if the numbers mix several), and cost and speed per run from log/runs.jsonl.

The cost line uses paid-tier list prices from config.py. On the free tier it costs nothing; the estimate tells you what it would cost. I eventually turned on billing for my own scout, because 20 calls a day left no room for testing. At five to eight calls a day it comes to a few dollars a month. You don’t need to do the same to follow this chapter.

The bars are Chapter 10’s “draft” bars. The scout stays at “suggest” until they pass for two weeks in a row, and it can’t leave anyway: there’s no code for anything else. When a number drops, read the log lines behind it, change one thing, bump PROMPT_VERSION, and measure again.

Stage 6: attack it

A careful model usually ignores the line and gives a low confidence. “Usually” is the important word: no prompt makes a model immune, and the result can change between runs. (Each run of this cell uses one of your daily calls.) Batching makes it slightly worse, too: in a real run this abstract shares a prompt with three other papers, so an injected line could nudge its neighbors’ verdicts as well.

Here are the defenses in the scout, from weakest to strongest:

DefenseIn the scout
Tell the model to ignore instructions in the datathe “data, not instructions” line in both prompts
Keep instructions and untrusted text apartthe papers sit inside <paper> tags, after the instructions
Validate the model’s outputthe schema, the range checks, the grounding check
Limit what the agent can dono publish, email or write action exists anywhere in the code
A person approves anything with impactthe scout only suggests; I decide what to read

I don’t rely on the model to spot every attack. I rely on the last two rows: if the model is fooled, the worst outcome is one wrong suggestion in my private issue list. That’s also why an agent that reads the open web should move up the autonomy dial slowly.

Other things that will happen in real use, and where to look:

  • Limits and outages. model call failed (ServerError), retrying in 30s means the model was busy; that’s the retry working. A daily-quota error stops the run and the digest says how many papers are waiting.

  • Missing full texts. The scorecard’s full_text_rate shows how often the scout decided from the abstract alone.

  • Model changes. The model behind the API can change without your code changing: Chapter 10’s Week C. The log records the model and the prompt version on every line, so you can compare before and after.

  • Scheduled runs pausing. GitHub may pause schedules in repos with no activity for a long time. The daily log commit usually counts as activity; if runs stop, re-enable the workflow in the Actions tab.

Stage 7: change it, and share what you change

Your copy is yours: change the interest, the guideline, the categories, and nobody else is affected. But a scout that only one person runs only gets tested on one person’s papers. Mine has only ever judged RAG papers, and I have no idea how the screen prompt does on, say, chemistry or economics. That’s a question you can answer and I can’t.

So the shared repo, open-paper-scout, takes pull requests. Three kinds of contribution, from least to most work:

  • Share your numbers. Once you’ve labeled 15 or more papers in your copy, python evaluate.py row prints one line with your interest, model, prompt version and scorecard. Add it to SCOUTS.md. Low numbers are as useful as high ones.

  • Fix or extend the plumbing. A new paper source, another model provider, a better digest, a test for a case I missed. The scout has three seams where most changes fit (the source, the ask() function, the prompts), and CONTRIBUTING.md spells out each one’s contract.

  • Change the judgment. A better prompt or guideline. This is the one with a rule attached: bump PROMPT_VERSION and show the scorecard before and after, on the same labels. It’s the course in one sentence: a fix without a measurement is a guess.

Some of the rules are enforced by code, not by me reading carefully. The tests.yml workflow runs test_scout.py on every pull request, and rejects one that adds personal files like log/ or labels.csv. It never sees an API key: GitHub doesn’t pass secrets to pull requests from forks, and the tests don’t need one. One rule is mine alone: the shared scout stays at “suggest”. Autonomy is earned per deployment, with that deployment’s scorecard (Chapter 10), so if yours earns “draft”, give it “draft” in your copy.

Here’s the workflow, so you can see how small it is:

If you want somewhere to start, CONTRIBUTING.md has a list, sorted by how much of the code you need to know. The one I’d most like someone to pick up is the calibration table: for every paper the scout rated 0.9, how often was it really relevant? I suspect mine is overconfident, and I haven’t measured it.

Is this still an agent?

Not in the strict sense of Chapter 3, and I chose that.

Chapter 3’s scoutThe deployed scout
Who picks the next step?the model (“read the full text of paper 0”)the code: fetch, screen, read, decide, always in that order
What does the model decide?which tool to use, and the verdictwhich papers pass the screen, and the verdict
Runs on its own?noyes, every weekday
Autonomy settingsuggestsuggest

The deployed scout is a fixed workflow with a model making the judgment calls. Chapter 9 moved the full-text decision into code because the model kept skipping it, and batching took the remaining tool choices away. For a job whose steps I know in advance, that’s cheaper, more predictable and easier to measure. If the scout had to decide for itself to follow citations or search related work, the agent loop from Chapter 3 would earn its place again.

Here’s the whole system as it actually runs:

Things I deliberately didn’t build: a judge model to label papers for me, a draft or publish step for the real scout, and any second agent. Each would add things that can go wrong, and I have no eval result yet that says any of them is needed. I also didn’t use an agent framework. Everything here is a few hundred lines of plain Python, and I wanted you to be able to read all of it.

Quick check

Before moving on, answer for yourself (no peeking ahead):

  1. Where does your API key live in Colab, and where in the deployed scout? Why not just paste it into config.py?

Check your reasoning ▸

In Colab it’s a notebook secret; in the deployed scout it’s a GitHub repository secret, passed to the workflow as an environment variable. Anything in config.py ends up in your repo and its history, where anyone with access can read it. A strong answer adds that if a key ever leaks, you delete it and create a new one.

  1. The scout asks Gemini for structured output with a Pydantic schema. Why does parse_verdicts still check the paper numbers?

Check your reasoning ▸

The schema only guarantees the shape: paper is an integer, confidence is a number. It can’t know that the batch has four papers, so paper 7 is valid JSON and still wrong. Range checks, clamping the confidence and the grounding check handle the meaning. A strong answer calls these two separate layers of validation.

  1. All nine tests in test_scout.py pass. What does that tell you about the scout’s suggestions, and what doesn’t it?

Check your reasoning ▸

It tells you the deterministic parts work: memory, the budget, the pending queue, validation, the grounding check. It says nothing about whether the model’s verdicts are right, because the tests use a fake model. That needs labels and the scorecard. A strong answer says you need both: tests to trust the plumbing, evals to trust the judgments.

  1. Why does the workflow run at 08:17 UTC and not at 06:17?

Check your reasoning ▸

Gemini’s daily allowance resets at midnight Pacific time, 07:00 or 08:00 UTC depending on the season. A run at 06:17 UTC would land in the previous day’s window, which earlier testing may already have used up. 08:17 UTC is always after the reset. A good answer connects this to the real 429 error that opens this chapter.

  1. A paper’s abstract tries to instruct the model. Which defense in the table would you rely on most, and why?

Check your reasoning ▸

The last two: the scout has no action that can do damage, and a person decides what happens with its suggestions. Prompt instructions and tags lower the chance of being fooled but can’t rule it out. Limiting what the agent can do caps the damage when it is fooled. A strong answer connects this to Chapter 5’s “limits in code, not in the prompt”.

  1. Someone opens a pull request on the shared repo that rewrites the verdict prompt and says the suggestions “look much better”. What do you ask for before merging?

Check your reasoning ▸

A bumped PROMPT_VERSION and the scorecard from evaluate.py before and after, on the same labels, with enough relevant papers labeled to mean something (Chapter 7). “Looks better” is a spot-check on a handful of papers. A strong answer also asks whether it was measured on more than one interest, since a prompt tuned on RAG papers can get worse on everything else.


You can now: run a small agent on real data within a call budget, validate structured model output, write software tests next to evals, deploy on a schedule with the key kept out of the code, measure quality and cost on real use, and take changes from other people without lowering the bar.

That’s the end of the course. The scout is small, and the loop behind it is the part I’d keep for any agent: narrow scope, real measurement, patterns before fixes, a metric for every failure you find, and autonomy earned one action at a time.

Where next: send your first pull request to open-paper-scout, even if it’s just your row in SCOUTS.md. Then, if the scout’s retrieval side made you curious, ◉ Grounded AI builds a RAG system from scratch, and ◈ Vector Reads covers a new LLM paper every week, many of them about agents.