The cost counter was incremented by a watcher that awaited the run task, so it lagged the status flip: the run read as done while /api/config still counted zero completed runs. The increment now happens in the same step as the transition to DONE, so any client that observes the run as done also observes the counter. Refs researchx#28
ResearchX
TL;DR — ResearchX turns one question into a cited research report: pick a method, approve the plan, and the engine visits real web sources, with every claim anchored to a quoted passage from a page it actually read. The engine milestone (v0.2.5) is shipped and owner-approved; hosting goes live next (v0.3, in progress).
- What — FastAPI backend (planning, search, extraction, verification, export gates) plus a Next.js frontend; the full test suite runs with zero API keys.
- Why — Readers can check every citation back to its source passage; runs are resumable from durable state.
- Action — no action — FYI. Developers: see the Quickstart below.
- Details —
frontend/README.md(web app);.ciagent/(pipeline state and roadmap).
A deep-research studio where you bring your own model, storage, and memory; choose a research methodology before a single query fires; and every claim in the final report is anchored to a visited, verified, quotable source.
Status: MVP shipped (v0.2.5, owner-approved 2026-09-27) — the full research engine (FastAPI + asyncio state machine + adapter layer + attack-proven trust loop) behind the v0.1 frontend. Live mode covers the core research loop (G-002/G-009): the engine runs locally per the quickstart; library/settings/suggestions/refine stay mock-backed until v0.3+.
Quickstart
# frontend (prototype, mock-backed — works today)
cd frontend
corepack pnpm install
corepack pnpm dev # http://localhost:3000
# backend (MVP — uv manages Python 3.12 and the venv)
cd backend
uv sync # installs deps + editable researchx package
uv run uvicorn researchx.api.app:app --reload --port 8000
uv run pytest # full suite, zero API keys
Three clicks from cold start to initiated research.
Monorepo command map
| Layer | Install | Dev | Gates |
|---|---|---|---|
| backend (uv, Python 3.12) | uv sync |
uv run uvicorn researchx.api.app:app --reload --port 8000 |
uv run ruff check src tests · uv run mypy src · uv run pytest |
| frontend (pnpm 12.6) | corepack pnpm install |
corepack pnpm dev |
corepack pnpm typecheck · corepack pnpm lint · corepack pnpm test · corepack pnpm build |
Engine architecture (v0.2 P2)
The research engine is a hand-rolled asyncio state machine:
PLANNING → AWAITING_APPROVAL → SEARCHING → EXTRACTING → REFLECTING ─┐
(halt until ↑ depth loop (dials.depth) │
approved) └────────────────────────────┘
→ SYNTHESIZING → VERIFYING → DONE (+ CANCELLED / FAILED terminals)
- Durable state:
runs/<id>/state.jsonrewritten atomically after every transition; resume re-enters at the recorded cursor with the persisted plan - Event bus: typed events (
run_started,query_fired,source_visited,passage_extracted,state_changed,run_done,run_error) — trace per run - Visited-only constraint: the writer's context is the evidence store and nothing else
- Trust loop: SSRF-guarded liveness (live/stale/dead), quote anchoring (normalize + fuzzy ≥ 0.85, weak matches flagged), citation ledger JSON, exportable verification checklist, single-source vs corroborated confidence labels
Run artifacts per run: state.json, report.md, ledger.json, checklist.md.
Try it on fakes (no keys): uv run python -m researchx.engine.demo
Adversarial guarantees (v0.2 P4)
Every trust-loop claim is attacked by the adversarial suite (backend/tests/test_adversarial.py) — these are tests that try to fool the verifier, and their passing proves the defenses:
| Attack | Defense (proven) |
|---|---|
| Writer hallucinates a citation to a passage that was never stored | Unknown passage IDs count as unanchored claims in the ledger; the export gate fails the report |
| Fabricated quote (matches nothing) | Anchoring verdict: no match, flagged — never silently accepted |
| Near-miss quote (0.80–0.85 similarity) | Flagged weak_match in the ledger — surfaced, not accepted |
| Liveness probe redirected to a private/metadata IP | Per-hop redirect re-guarding classifies it dead (SSRF refusal) |
| Redirect chain ending at a gone page (410) | Classified dead |
| Oversized page (5MB) response | 2MB read cap — classifies without unbounded memory |
| Pause during the depth loop | Durable state persists; resume completes the run over the HTTP API |
| Cancel mid-loop | Terminal CANCELLED state persisted, never swallowed |
REQ-23 (≥99% citation liveness) is a per-run export gate enforced by the verification checklist, plus an opt-in keyed live tier (cd backend && uv run pytest -m live) — the zero-key CI suite never asserts it vacuously.
The E2E happy path (≤3 clicks: ask, choose, approve) is validated against the real FastAPI + engine stack, defaults only — not the mock (frontend/src/lib/api/e2e-real-stack.test.ts).
Adapter configuration (v0.2)
All keys resolve from env files only (.env, .env.secrets, .env.* — never the shell environment, never git):
| Variable | Scope | Used by |
|---|---|---|
OLLAMA_API_KEY |
model (cloud) | Ollama cloud endpoint https://ollama.com/v1; absent/keyless falls back to http://localhost:11434/v1 automatically |
OLLAMA_BASE_URL |
model | Overrides the default cloud base URL |
TAVILY_API_KEY |
search | Tavily default search; absent falls back to the keyless DuckDuckGo adapter (degraded mode — surfaced in the run trace) |
The test suite needs no keys: fake model/search adapters (REQ-31) drive every engine test.
Live mode (v0.2 P3)
The frontend runs mock-backed by default (works standalone, zero keys). To run the core research loop against the real backend:
# terminal 1 — backend (fake adapters by default; real Ollama/Tavily adapters
# are used by the engine in v0.2.5+ wiring — the served API currently ships
# fake-adapter runs for demos and tests; see Adapter configuration below)
cd backend && uv run uvicorn researchx.api.app:app --reload --port 8000
# terminal 2 — frontend in live mode
cd frontend && NEXT_PUBLIC_RESEARCHX_API_MODE=live corepack pnpm dev
Live mode covers the core loop (Ask → Choose → approve plan → live trace → report + ledger); the library, settings persistence, suggestions, refine, and graph surfaces stay mock-backed until v0.3. The API surface: POST /api/runs, GET /api/methodologies, run lifecycle (approve/pause/cancel/resume), GET /api/runs/{id}/events (SSE), GET /api/config, report + ledger artifacts.
What's in the prototype
| Screen | What it does |
|---|---|
/ask |
One question box + one optional context box — plus two opt-in helpers below and beside them: open-question suggestions (follow-ups from your past runs, one click fills the box) and the Refine question dialogue (a few clarifying answers → a composed, sharper question). Neither ever auto-starts. |
/choose |
All 8 locked methodology cards — mock ETA, cost estimate, why-recommended, and the 12 methodology dials behind each card's Advanced drawer. |
/run/[id] |
Plan & Run: approve the research plan (sub-questions, source universe, outline), watch the live trace stream, pause/resume or cancel. |
/run/[id]/report |
Reading Room: the report with page-anchored citations — hover or focus any [n] marker for the exact quoted passage — plus the citation ledger and Markdown/JSON exports. |
/library |
Stored reports filed into auto-categorized folders (sortable, links into the Reading Room), the Connections panel (shared sources ⇄, related topics →), plus v2-preview project graph and task inbox. |
/settings |
API-key status (masked, never raw), the reports storage directory, methodology defaults with the resolved 12 dials, appearance, and about/version. |
The persistent shell (every screen): sidebar nav + adapter connection-status dots, top bar with the ⌘K navigation palette (screens + stored reports) and the avatar menu (local profile, Settings, About).
All data flows through a typed ApiClient interface backed by MockApiClient — the same components will talk to the FastAPI backend via the OpenAPI-generated client in the MVP milestone, with zero rewrites.
Scripts
Run inside frontend/:
| Command | Purpose |
|---|---|
pnpm dev |
Dev server on :3000 |
pnpm build |
Production build |
pnpm lint |
ESLint (flat config) |
pnpm typecheck |
tsc --noEmit (strict) |
pnpm test |
Vitest + Testing Library (jsdom) |
Repository layout
frontend/ Next.js 15 App Router, TypeScript strict, Tailwind v4, Vitest
.ciagent/ Pipeline state (roadmap, requirements, plans, checkpoints)
Milestone v0.2 (MVP: FastAPI backend, research engine, adapter layer, trust loop) begins only after this prototype is approved.