Field notes · what I learned building it

Agentic Briefing Field Notes

The engineering ideas I ran into while building a multi-source, agentic AI news-briefing pipeline — each with the plain lesson and the line to say out loud in an interview.

Project Daily_News — audio briefing on Cloud Run Stack Python · Gemini on Vertex · ADK · Cloud TTS Captured Sept 2026
01

Architecture & design patterns

One app, a swappable brain — the Strategy pattern

I didn't build a "deterministic app" and an "agentic app." I built one pipeline where every stage is shared except the curation step, which sits behind a Curator abstract base class. A deterministic ranker and an ADK agent both implement the same curate() method, so a config flag swaps them without touching anything else.

Say this in an interviewI used the Strategy pattern so the deterministic and agentic curators were interchangeable behind one interface — comparing them was a config flag, not a rewrite.

Abstract base classes are a contract, not just inheritance

An ABC does two things at once: it lets me add new strategies freely (extensibility), and @abstractmethod makes the program refuse to run a subclass that forgot to implement the method (an enforced contract). The same idea gave me a Source ABC so Gmail, RSS, Hacker News, and a stock API all return the same normalized Story object — the adapter pattern.

Say this in an interviewI normalized every source into one schema behind a Source interface, so the pipeline downstream was completely source-agnostic — adding a feed was a new adapter, not a pipeline change.

Open/Closed & config-driven design

New topics get added in a feeds.yaml file, not in code — the engine is open to new behavior but closed to modification. The feed catalog (sources, word budgets, prompts) is data; the engine that runs it is code.

Segment-then-stitch beats one mega-prompt

For a combined digest, the wrong move is dumping every source into one giant prompt — each topic needs its own voice, and you blow the token budget. Instead each topic is rewritten with its own persona and word budget, then a composer stitches the segments with spoken transitions. Adding a topic doesn't change the composer.

Deterministic backbone, agent only where judgment pays

The delivery path (fetch → rewrite → TTS → email) stays deterministic because it's an unattended cron job that must be reliable. The agent lives only in curation, where dynamic judgment actually helps — and if it errors, it falls back to the deterministic curator so the digest always ships.

Say this in an interviewI used an agent only where dynamic judgment added value and kept the delivery path deterministic for reliability and cost — with the deterministic version as an automatic fallback.
02

LLMs, agents & evaluation

LLM-as-judge: the model grades output against source in one prompt

To judge a segment, I put both the source stories and the produced script into a single prompt and ask the model to score faithfulness, coverage, and relevance. It needs both halves: you can't catch a hallucination without the source to check against, and you can't catch a missed story without seeing what could have been included.

Reference-free vs reference-based judging

Reference-free: the judge reasons from the source material alone. Reference-based: the judge grades against a human-written answer key (expectations.md) — my must-includes, noise to drop, duplicates to merge, and red lines. I wired the answer key into the judge so my editorial judgment became part of the automated score.

Say this in an interviewI built both reference-free and reference-based evals — the reference-based one grades output against a golden answer key I wrote, so the score reflects real editorial intent, not just internal consistency.

Golden datasets = frozen input + an answer key

A golden set is captured source stories (sources.json, frozen so it never changes) plus my expectations. Because the input is fixed, when I tweak a prompt any change in the score is caused by my change, not by the news being different that day. That's what makes an eval a fair, repeatable test.

An LLM can't browse unless you give it a tool

A plain generate_content() call has no internet access — it only sees the text in the prompt. A URL in the data is just characters; the model can't click it. Web access comes only from a tool (my agentic curator has a fetch_article() tool; the judge deliberately doesn't).

Gotcha My faithfulness check is only as deep as the text I captured. With one-line RSS snippets, the judge can flag legitimate detail as "unsupported" simply because it isn't in the snippet — not because it's false. Richer captured text is the fix, not a smarter judge.

Not all metrics survive thin sources

Coverage and relevance hold up with headline-only sources — a headline is enough to tell whether a story was included and on-topic. Faithfulness does not — verifying invented details needs the full article text.

Agentic memory: long-term state exposed as a tool

A stateless daily job can't say "Y beats last week's X" because it never saw X. I persist a compact record after each run and give the agent a recent_digest_history() tool to read it. That cross-run memory — distinct from the agent's within-run context — is what enables competitive comparisons.

Vertex AI vs the AI Studio API-key path

The same Gemini SDK can talk to two backends: Vertex AI (uses your cloud project + ADC credentials) or AI Studio (needs an API key). ADK defaulted to the API-key path and failed with "No API key provided" until I set GOOGLE_GENAI_USE_VERTEXAI=True plus project and location.

Gotcha "No API key was provided" from an agent usually doesn't mean you need a key — it means the SDK isn't pointed at Vertex. Fix the routing, don't add a key.

Evals cost real tokens — and the judge shouldn't grade its own family

One eval run is N generation calls + N judge calls (plus curation if agentic) — roughly 8–12 model calls for four segments, doubled when comparing two curators. And using the same model to generate and to judge invites self-bias; a common practice is a stronger or different judge model.

03

Text-to-speech & audio

TTS has a per-request input limit — so you chunk and stitch

Every TTS engine caps input per request, because one call is meant for a chunk of speech, not a whole document. A ~1,000-word briefing can't go in one call — so the code splits the script on sentence boundaries, synthesizes each chunk, and concatenates the audio bytes into one file.

PathPer-request limit
Gemini TTS4,000 bytes
Standard synthesize_speech (Chirp3-HD, Neural2)5,000 bytes
Long Audio API (async, writes to GCS)~1,000,000 bytes

The limit is in bytes, not characters

This one bit me. A — em-dash is 3 bytes; a curly quote is 3 bytes. A 4,500-character script can be well over 5,000 bytes, so my chunker had to measure UTF-8 byte length, not len().

Say this in an interviewThe TTS limit is measured in bytes, not characters, so multi-byte punctuation like em-dashes can push you over even when the character count looks safe — I made the chunker byte-aware.

Selecting a voice vs selecting a model

In Cloud TTS, VoiceSelectionParams takes a voice name and language_code. To use Gemini TTS you also pass model_name (only in library ≥ 2.29.0) and can add a style prompt on SynthesisInput. Passing a model into the wrong field — or as the old model= — just errors.

The voice model is not the "it sounds robotic" lever

I A/B'd a premium Gemini TTS voice against Chirp3-HD and honestly couldn't tell the difference — so I dropped the preview model (it's unstable and forced that 4,000-byte limit) for the stable voice. "It feels like it's just reading the screen" is a script and format problem — the fix is conversational writing or a two-host dialogue, not a fancier voice.

Say this in an interviewI tested a premium TTS model, measured no perceptible gain over the stable voice, and cut it — naturalness comes from the script and format, not the voice tier.
04

Infra, Git & engineering judgment

Source control and deployment are separate systems

Pushing to GitHub stores and versions code; it runs nothing. Deploying to GCP (via deploy.sh → Cloud Build → Cloud Run) is what actually runs it — and it deploys the code in my working folder, not what's on GitHub. Safe order: merge the PR → pull main → deploy.

Push vs pull request; one working tree

A push uploads a branch to GitHub; a pull request is a separate proposal to merge that branch into main, reviewed and accepted with a Merge button. And all branches share one working folder — uncommitted edits float and aren't tied to a branch, which is why Git blocks a switch that would overwrite them (commit or stash first).

Word budgets are where editorial priority lives

The per-segment word budget does four jobs at once: it controls total listen time (~150 words/minute), allocates attention by topic (AI gets more than markets), forces concision so the model prioritizes instead of dumping, and gives the eval a concrete target to score.

Knowing when not to add a model is seniority

My A/B result was honest: for a typical day, the agentic curator improved relevance and competitive framing but didn't justify ~2× the latency and cost, and both curators shared a coverage gap I traced to ranking. Reporting that plainly — instead of declaring the fancy thing a win — is the mature move.

Say this in an interviewI A/B'd an agentic curator against a deterministic baseline with an eval harness. The agent improved relevance and framing but didn't justify 2× latency on typical input, and both shared a coverage gap I traced to ranking — so I kept the baseline as default and the agent behind a flag.