Inspiration

Every agent platform sells "human in the loop." Nobody measures whether that human is still there.

When an agent fleet asks for approval fifty times a day, the first request gets read, the tenth gets skimmed, and the fiftieth gets stamped before the sentence is finished. The control still exists on paper. It stopped meaning anything somewhere around request twelve, and nothing in the stack noticed, because every system in this space records that a human approved and none of them record whether the approval meant anything.

Attackers already exploit this. The pattern is catalogued as ATR-2026-00118, Human Approval Fatigue Exploitation: flood the queue, then hide the dangerous action where a tired reviewer will wave it through.

And regulation is about to ask for the proof. EU AI Act Article 14 requires human oversight that is effective rather than decorative. That obligation lands on 2 December 2027, moved there from August 2026 by the Digital Omnibus on AI (Regulation (EU) 2026/1744, in force 27 July 2026). Article 99 puts non-compliance in the 15 million euro or 3% of global turnover tier. Nobody has agreed on how you would prove the difference.

Watchspan proves it with numbers.

What it does

Watchspan is a gate between an agent fleet and the people who approve what it does. It treats reviewer attention as a finite, replenishing resource, prices every approval against it, and declares with evidence the moment oversight decays into rubber-stamping.

In one thirty-minute seeded run it routed 370 actions: 294 ran on their own with an audit log, 7 were held by the Sentinel as attack patterns, and 69 went to a human. Of those 69, 14 were decided with attention left to give. Oversight was declared degraded at 05:06, and 51 high-risk actions were approved with zero seconds of reading behind the click.

Then it turns the same instrument on the person watching. The control room serves twelve real approval requests, one at a time. The server issues the reviewer identity, starts the clock when it hands a card over, and counts the detail sections actually opened. Nothing in the request body can set any of the three. Stamp your way through and it tells you, in your own seconds and under your own reviewer id, the moment you stopped reviewing.

How we built it

A deterministic core with a language model on top, never underneath.

Four governance agents (Meter, Drift, Calibrator, Sentinel) are deliberately plain Python. Every routing and drift decision is computed and reproducible from a seed. Gemini 3.5 Flash writes the findings on top of numbers it did not produce. A governance layer whose verdicts move when the model has a bad day is not a governance layer.

The attention budget charges each review by how much reading it demands:

$$\text{cost} = C_{\text{base}}\left(1 + \sigma \cdot k\right), \quad C_{\text{base}}=1.0,\ \sigma=2.0,\ k \in [0,1]$$

so a dense action costs three times a routine one, against a pool that replenishes at 0.5 units per minute.

The escalation bar then rises as the pool empties:

$$\theta(b) = \min\left(0.95,\ \theta_0 + \beta(1 - b)\right), \quad \theta_0 = 0.30,\ \beta = 0.40$$

where $b$ is the fraction of budget remaining. A drained reviewer gets fewer interruptions, better attended. The counterintuitive part is that this is safer, and we measured it rather than asserting it.

The claim is never the input. Every request arrives with a risk score the calling agent gave itself, and Watchspan routes on

$$r_{\text{routed}} = \max\left(r_{\text{declared}},\ r_{\text{assessed}}\right)$$

with $r_{\text{assessed}}$ computed from what the action says it does. An action it cannot classify is escalated rather than cleared, because an unknown blast radius is not a safe one.

On Google Cloud, checkably. ADK 2.7.1 builds the fleet; the coordinator calls agents:search against the Agent Registry at startup and instantiates what comes back, so unregistering an agent removes it without a code change. Model Armor screens every model call as a before_model_callback. The ledger lives in Memory Bank. Every routing and human decision is a Cloud Trace span carrying the numbers that justified it. Agent Runtime hosts the deployed engine. Everything runs on Cloud Run, scaled to zero.

GET /geap/status calls all of it in one unauthenticated request and reports what came back, labelling each answer as a live round trip or a config check and refusing to count the second as the first. Six of six answer live.

Challenges we ran into

The agent was grading its own homework. A reviewer ran our live fleet three times and watched the same action come back self-scored 0.40, 0.50 and 0.30. On the third run Watchspan auto-executed a production table drop, because the agent said it was routine. Our headline safety property was being set by the entity it governs, which is the exact argument we make about the reviewer. That is why independent assessment exists.

Then the fix was defeated twice. Renaming change_vendor_bank_account to update_supplier_remittance_details walked straight through a keyword list. Fixing that opened the mirror hole: naming a dangerous action to resemble a benign catalogued one, so update_vendor_contact_details_new_iban inherited a 0.10 and auto-executed. The lesson was that objects are not one kind of thing. An IBAN, a credential and a privilege are dangerous with no verb at all; "customer" and "production" only raise whatever verb they attach to; "staging" and "cache" say out loud this is not the real thing.

The console passed a rubber-stamper. Someone took the reviewer queue, stamped all twelve blind, and was told oversight holding. The verdict was reading a pooled team window where our own simulator's careful decisions outnumbered theirs. The product whose reason to exist is detecting rubber-stamping watched someone rubber-stamp everything and passed them.

A judge's first click waited thirteen seconds. The seeded run itself takes 2.5. The rest was a blocking Gemini call writing a narrative no screen displays. Moved off the critical path: 13.6s to 1.2s.

And we published numbers the code had stopped producing. The demo film said "thirty-four high-risk actions" and "35% longer" when the experiment produced 33 and 29.9%. On a project whose entire argument is that unverified assertions are worthless, that was the most expensive mistake available. Every figure the film speaks is now fetched from the running API or the experiment at build time, asserted against the narration before a frame renders, and gated in CI.

Also, for anyone else building on this stack: gemini-3.5-flash is served only from the global location and returns 404 in us-central1; the Agent Registry search field is searchString, not query; a PATCH without an updateMask returns 200 and changes nothing; ADK does not inherit before_model_callback from a parent, so every agent needs its own; and Cloud Run's --session-affinity is inert unless the browser actually sends credentials.

What we learned

Escalating less can be safer than escalating everything, but only with a floor. Raising the bar from 0.30 to 0.45 cut interruptions from 69 to 62 and held oversight about 30% longer. It did not produce more attentive reviews: the count stayed at 14 either way, and the ratio improved only because the denominator shrank. Without a floor, 33 actions above risk 0.70 would have run unseen. ALWAYS_ESCALATE_ABOVE = 0.7 exists because of that measurement, and with it in place the number is zero.

Consensus between agents is not safety. We added peer review between the fleet's agents, binding upward only, so a colleague can raise a score and never lower it. Both times we ran it live, the peer agreed with the proposer at 30 and Watchspan overruled them both at 75. That result is in the demo, unedited, because two agents agreeing is exactly the failure mode we describe in one tired human.

Adversarial review finds what testing does not. Four independent reviewers probed the deployed service and defeated both headline safety properties with curl. Everything above under Challenges came from that, not from our own tests. The suite grew to 47 and now gates the figures the film and the README publish, so a silent drift in any of them fails the build rather than reaching a judge.

Measure rather than trust the eye. The demo film was 83% frozen by frame diff while looking fine. A page-zoom that silently did nothing reported success. A deployed revision failed to start while the old one kept serving and the exit code said zero. Every one of those was invisible until something counted it.

Built With

  • a2a
  • agent-identity
  • agent-registry
  • agent-runtime
  • cloud-run
  • cloud-trace
  • css
  • fastapi
  • gemini-3.5-flash
  • google-adk
  • memory-bank
  • model-armor
  • next.js
  • opentelemetry
  • pytest
  • python
  • react
  • remotion
  • tailwind
  • typescript
  • vertex-ai
Share this project:

Updates