Skip to content

feat(experiments): shadow mode — observe-only candidate fan-out (Step 5.7b) - #145

Merged
officialCodeWork merged 2 commits into
mainfrom
build/phase-5/step-5.7b-shadow-mode-claude
Jun 7, 2026
Merged

feat(experiments): shadow mode — observe-only candidate fan-out (Step 5.7b)#145
officialCodeWork merged 2 commits into
mainfrom
build/phase-5/step-5.7b-shadow-mode-claude

Conversation

@officialCodeWork

@officialCodeWork officialCodeWork commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Summary

Step 5.7b — shadow mode: on a sampled fraction of live /v1/query retrievals, run a candidate retriever alongside the served control path observe-only (never affecting the response) and feed both variants' outcome into the 5.7a ABExperimentTracker, so GET /v1/status/experiments compares them on real traffic before anyone serves the candidate.

This is the consolidated, final 5.7b — correct to merge. It is the functional superset of the two parallel implementations and supersedes the WIP on build/phase-5/step-5.7b-shadow-mode (20c1f58, no PR). Based on main after the TRACKER restructure (#144), so it merges clean (origin/main is an ancestor — no rebase, no conflicts).

Design — hot-path safe (the Phase-4 reliability invariant, applied to A/B)

  • Never delays the response — the candidate runs in a FastAPI BackgroundTask, after the response is sent.
  • Degrade-open — any candidate failure is logged (experiment.shadow_failed) and dropped; a crashing candidate still returns 200.
  • Deterministic samplingsha256(request_id) < shadow_sample_rate, stable across processes; skipped on a retrieval-cache hit (no fresh retrieval to compare).
  • Same read_chunk PDP — the candidate routes through HybridRetriever, so ACLs are enforced; no PolicyEngine coverage-linter entry needed. Event-only observability (no per-call span).

Key choices (best of both parallel implementations)

  • ShadowRunner (rag_gateway.experiments) — isolated + unit-testable, and keeps ABExperimentTracker a pure sample holder per ADR-0032 §3 (sampling lives in the gateway, not in observability).
  • Records both variants atomically (only after the candidate succeeds) so the comparison windows stay balanced even when a candidate flaps.
  • Config-driven candidatecfg.experiments.shadow_candidate RRF weights build a real second RetrievalRouter, so shadow works end-to-end from rag.yaml; production can also inject one via build_app(shadow_runner=…).
  • cfg.experiments gains shadow_enabled / shadow_sample_rate / shadow_experiment / shadow_candidate; doubly opt-in (enabled AND shadow_enabled) and observe-only, so turning it on cannot change a served response.
  • ragctl shadow smoke command (drives the full flow against in-process stubs, creds-free).

Verification — all gates green

ruff · mypy --strict (292 files) · RAG001 · log-schema/event-registry · policy-coverage · schema-drift · openapi-drift (unchanged — BackgroundTasks is not a body/query param) · gateway / config / observability / ragctl / contract / retrieval / core suites · 27 new tests.

Docs

ADR-0032 (5.7b landed) · architecture/ab-shadow-mode.md · reference/experiments.md · docs/README.md · TRACKER (5.7b ✅, Next action → 5.7c).

Test plan

  • Sampled query records both variants; zero-rate / unwired is a no-op; crashing candidate still returns 200
  • Config-driven wiring builds the runner only when enabled + shadow_enabled
  • Deterministic sampling; outcome metric (mean retrieval score) units
  • Merges clean against current main

🤖 Generated with Claude Code

Deep Kumar Singh Kushwah and others added 2 commits June 7, 2026 17:33
… 5.7b)

Run a candidate retriever alongside the served control path on a sampled
fraction of live queries — observe-only — and feed both variants' outcome
(mean retrieval score) into the 5.7a ABExperimentTracker, so the dashboard
compares them on real traffic before anyone serves the candidate.

- ShadowRunner (rag_gateway.experiments): scheduled as a FastAPI BackgroundTask
  so the served response is never delayed; degrade-open; deterministic
  request_id-hash sampling; skipped on a retrieval-cache hit.
- Candidate is any SupportsRoute; the config build makes a RetrievalRouter
  differing only in shadow_candidate RRF weights; production injects one via
  build_app(shadow_runner=...). Same read_chunk PDP, event-only observability.
- cfg.experiments gains shadow_enabled / shadow_sample_rate / shadow_experiment
  / shadow_candidate; doubly opt-in (enabled AND shadow_enabled). ragctl shadow.
- 27 tests; ruff / mypy --strict (292) / RAG001 / log + policy gates green;
  rag.schema regenerated (no dist/schemas or openapi change).

Docs: ADR-0032 (5.7b update), reference/experiments.md, architecture/ab-shadow-mode.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@officialCodeWork officialCodeWork changed the title feat(experiments): shadow mode (Step 5.7b) — alternative impl for comparison feat(experiments): shadow mode — observe-only candidate fan-out (Step 5.7b) Jun 7, 2026
@officialCodeWork
officialCodeWork merged commit 46a2d10 into main Jun 7, 2026
12 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant