fix: enforce search_web's cutoff date from the harness, not the LLM#162
Merged
Conversation
PR #161 added an independent leakage verifier to search_web, but it only activated when the calling LLM supplied a cutoff_date tool argument. A production Langfuse trace (3ca3e806410f55615c424719e2a3ecbc, WTI news agent, as_of=2026-05-18) showed this bypassed outright: 11 of 14 search_web calls omitted cutoff_date entirely, so both the soft prompt instruction and the verifier were silently skipped, and raw live search results (dated "as of July 1, 2026" — ~6 weeks after the backtest origin) flowed straight into the forecast. The cutoff is now seeded into ADK session state by AgentPredictor.predict() (as AS_OF_STATE_KEY) before each run, and search_web reads it via an ADK-injected ToolContext — a parameter excluded from the LLM-visible tool schema, so the model can neither see, omit, nor spoof it. This harness value is authoritative whenever present; the LLM-supplied cutoff_date argument is now a redundant, non-load-bearing fallback for callers outside AgentPredictor (e.g. interactive adk web use), with a warning logged if the two disagree. AgentPredictor and ForecastContext.as_of are shared across every forecasting domain, so this closes the bypass everywhere in one change — no per-domain edits needed this time (unlike #161's prompt-hardening propagation). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…place Re-ran the WTI news-agent eval spec (both models) after clearing only their stale eval cache. Confirmed via Langfuse traces: every search_web call now runs the search->verify pair regardless of whether the LLM passed cutoff_date, harness as_of correctly overrides the raw googleSearch result in every sampled case, and one concrete instance was traced end to end — a raw result dated "as of July 2, 2026" (~6 weeks past the as_of=2026-05-18 origin) was flagged, stripped, and confirmed absent from the analyst's final forecast rationale. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ethancjackson
added a commit
that referenced
this pull request
Jul 4, 2026
Extend the Analyst Agent session (16 -> 23 slides) with an evidence-driven leakage war story that pays off the existing hard/soft-cutoff beat: - The smoking gun: CRPS-by-horizon, pre-fix flat vs post-fix fanning (the leaked agent's error didn't grow with horizon). - How we caught it (fingerprint + trace), three tries to fix it (prompt -> independent verifier #161 -> harness-enforced cutoff #162), one polluted vs one self-corrected run, the "can't un-leak a live backtest" warning, and a positive close on live evaluation (ForecastBench). - New systems diagram of how the analyst / news sub-agent / search_web / independent verifier / harness cutoff fit together and check each other. Figures are real committed run outputs, parsed not hand-typed: - leakage_crps_by_horizon.png: post-fix recomputed from committed eval YAMLs; pre-fix read from the NB04 blob at d068737 (fails loud if absent). - agentic_system.png: conceptual; every box maps to a real component. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #161. That PR added an independent leakage verifier to
search_web, but it only activated when the calling LLM supplied acutoff_datetool argument. A production trace showed this bypassed outright: 11 of 14search_webcalls in a real backtest simply omittedcutoff_date, so both the soft prompt instruction and the verifier were silently skipped, and live googleSearch results (dated the actual current date) flowed straight into a historical forecast whose origin was six weeks earlier.This PR closes that bypass by moving the cutoff from something the model supplies to something the harness enforces:
AgentPredictor.predict()now seeds the forecast'sas_ofinto ADK session state before each run, andsearch_webreads it via an ADK-injectedToolContext— a parameter excluded from the LLM-visible tool schema, so the model can neither see it, omit it, nor spoof it. This value is authoritative whenever present; the model's owncutoff_dateargument becomes a redundant, logged-on-disagreement fallback for callers outsideAgentPredictor(e.g. interactiveadk webuse).Because
AgentPredictor/ForecastContext.as_ofare shared across every forecasting domain, this closes the bypass everywhere in one change — no per-domain edits needed this time.Clickup Ticket(s): N/A
Why this needed a second agent, not a better prompt
Both leaks in this saga (this one and #161's) trace back to the same root cause: a single LLM cannot reliably introspect on where its own words came from — whether a claim was retrieved just now or recalled from training. No amount of prompt engineering fixes that, because the model isn't lying about its confidence, it genuinely can't always tell the difference. The fix that actually works is architectural: a second, independent LLM call whose only job is to audit the first one's output against an external fact (the cutoff date) it wasn't in the loop to negotiate. That's a real agentic decomposition — specialized sub-agents cross-checking each other — not just a bigger prompt on a single model. This mirrors the published TimeSPEC pattern (claim-level extraction + independent verification beats prompt-only cutoff instructions by 75-99% in their evaluations).
The caveat that matters just as much: this is harm reduction, not a substitute for live evaluation. A verifier can only catch what it's told to look for and reason correctly about; it cannot conjure certainty from a fundamentally leaky retrieval channel (the live web index doesn't stop existing at the backtest origin). The only setup where future-leakage is structurally impossible, not just guarded against, is genuine live evaluation — predicting into a future that hasn't been indexed anywhere yet. Backtesting an agent with live web search, however well-guarded, is always a step down from that in terms of what it can prove.
Changes Made
aieng-forecasting/aieng/forecasting/methods/agentic/agent_factory.py:search_webnow accepts an ADK-injectedtool_context: ToolContext; reads the harness-seededas_of(newAS_OF_STATE_KEY) as the authoritative cutoff, falling back to the LLM'scutoff_dateonly when no harness value exists. Logs a warning if the two disagree.aieng-forecasting/aieng/forecasting/methods/agentic/adk_runner.py:run_text_async/_resolve_session_idgained an optionalinitial_stateparameter, threaded into ADK'screate_session(..., state=...)— purely additive, every existing call site unaffected.aieng-forecasting/aieng/forecasting/methods/agentic/predictor.py:AgentPredictor.predict()seeds{AS_OF_STATE_KEY: context.as_of}into the session before each run.as_ofpresent, LLM omitscutoff_dateentirely) and the disagreement-logging path.Testing
uv run pytest tests/)uv run mypy <src_dir>)ruff— learned that lesson on fix: independent temporal-leakage verifier for search_web #161)Manual testing details:
uv run pytest aieng-forecasting/tests/aieng/forecasting/methods/agentic -q— 95 passed, no regressions.uv run pytest tests/(fullaieng-forecastingsuite) — 387 passed, 7 skipped.python -m pytest implementations/tests -q— 82 passed.search_webcalls (36 traces, both models): 0 occurrences of[SEARCH_VERIFICATION_FAILED], 0 calls with retry-range latency — everything passed on the first attempt.as_of=2026-05-18, raw search result dated "As of July 2, 2026" (~6 weeks post-cutoff, the same leak class as before) — the verifier flagged 6 claims, stripped them, and the analyst's finalset_model_responserationale contains no trace of the July content. This is the exact scenario that slipped through silently before this fix.search_webspan now displays itsinput/outputmirroring the raw first child call rather than the tool's actual arguments/return value — likely because ADK's OTEL instrumentation can't cleanly serialize the newtool_contextparameter. Confirmed via source code and the final rationale that actual runtime behavior is correct; only the trace display is affected.Related Issues
Follow-up to #161.
Deployment Notes
The 2025 backtest prediction files for these two predictors were not re-run in this PR (only the smaller 2026 eval spec, per the new "verify before opening" process) — a full backtest rerun can follow separately if wanted, but isn't required for correctness since the fix is unconditional at the code level.
Checklist