Meet Scout 🔎 — a research agent that shows its work. Ask it a question; it plans sub-questions, searches each one, pulls out individual claims, scores where each claim came from, and — the part most research agents skip — tells you when its own sources disagree instead of quietly picking one. "I don't trust one source. Neither should you."
$ triangulate "Is coffee good or bad for cardiovascular health?"
╭─────────────────────────────────────────────╮
│ Scout │
│ I don't trust one source. Neither should you.│
╰─────────────────────────────────────────────╯
Scout: New case! Let me break this down first.
Scout: Here's how I'm splitting this up:
1. Is coffee linked to lower CVD risk?
2. Does caffeine raise blood pressure?
Reading up on: Is coffee linked to lower CVD risk?
found 3 source(s)
pmc.ncbi.nlm.nih.gov 0.90 · high
health.harvard.edu 0.90 · high
sciencedaily.com 0.45 · unknown
⚠ Scout: Wait — these two sources don't agree. Flagging it, not picking a side.
pmc.ncbi.nlm.nih.gov vs sciencedaily.com on coffee_cvd_risk
Scout: Investigation complete. A few sources disagreed — worth a closer read.
Investigation summary
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Sub-questions ┃ Claims ┃ Contradictions ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ 2 │ 2 │ 1 │
└───────────────┴────────┴────────────────┘
PASS — 5 checks, 0 violations
Most "research agent" demos do one search, summarize the top results, and state the answer with total confidence — even when two of those results actually disagree, or one of them is a random blog post and the other is a peer-reviewed study. That's not research, it's a weighted coin flip with extra steps.
triangulate makes three things explicit that are normally invisible:
- The plan. A question gets broken into sub-questions before any searching
happens (
research/planner.py) — you can see exactly what it decided to investigate, and why a broad question got broad coverage instead of one lucky search. - Where every claim came from. Nothing is stated without a source attached
(
research/extractor.py,research/store.py) — the report links every sentence back to the page it was pulled from. - Whether sources agree. Claims about the same fact
(
subject_key-matched) are checked against each other (research/contradictions.py) — numerically (do the numbers actually match?) and textually (do independent sources actually say the same thing?). A contradiction is reported, never silently resolved by picking whichever source came first.
Underneath, every source is scored for credibility before its claims are trusted
(research/credibility.py) —
domain tier (.gov/.edu/peer-reviewed journals score highest), corroboration
across independent domains, and recency. The score travels with the claim into the
report, so "coffee helps your heart" from nih.gov and the same sentence from a
random blog are not treated the same.
uv venv && uv pip install -e '.[dev]'
export ANTHROPIC_API_KEY=...triangulate "What caused the 2010 flash crash?"triangulate-dashboard # -> http://localhost:8420A local-only page (127.0.0.1, nothing framework-based — stdlib http.server and
plain JS) that polls the same SQLite file the CLI just wrote to and redraws every
2 seconds: the question tree, every source sorted by credibility with a color-coded
badge, and any contradiction found, pulled out into its own red card so it can't be
missed by skimming. A live trace feed at the bottom shows the raw pipeline events —
plan, search_done, source_scored, claim_extracted, contradiction_found — as
they happen. Run the CLI in one terminal and the dashboard in another to watch an
investigation build in real time.
Every pipeline stage emits an event (investigate(..., observer=...)), and the CLI
wires that same event stream into two places at once: the terminal (Scout narrating
live) and a JSONL trace file, .triangulate/traces/<date>.jsonl — one line per event,
replayable after the fact.
After every run, ops/eval.py runs a small gate
of deterministic checks directly against the investigation's data — every claim has a
real source, every credibility score is in range, every contradiction actually links
two distinct claims about the same subject. This is not a judgment call about
whether the research was good ("was this a helpful answer?" is a real, different
question, and deliberately isn't what this checks) — it's the cheap, structural
"did the pipeline do what it's supposed to do" check, the kind of thing that catches a
real bug (one already did, during development — see the commit history) rather than
grading the model's prose.
Or from Python:
from triangulate_agent import connect, ResearchStore, investigate
from triangulate_agent.models import get_client, DEFAULT_MODEL
from triangulate_agent.research.report import render_report
store = ResearchStore(connect(".triangulate/case.db"))
findings = investigate(get_client(), DEFAULT_MODEL, "claude-haiku-4-5",
"What caused the 2010 flash crash?", store)
print(render_report(store, findings.root_question_id,
"What caused the 2010 flash crash?", findings.sub_question_ids))Everything lands in one SQLite file (.triangulate/case.db) — open it directly:
sqlite3 .triangulate/case.db '.tables'. questions (the plan tree), sources
(every URL touched, with its credibility score), claims (one row per extracted
assertion, linked to both), contradictions (pairs of claims that disagree).
Credibility scoring, contradiction detection, and report rendering are pure
functions — no API calls, fully covered by tests/, and safe to run
against fixture data with no key set. Only decomposition (planning) and claim
extraction call the model, and both fail open: a broken decompose call means
the question runs unsplit, not that the investigation crashes; a page that yields
nothing extractable just contributes zero claims.
uv run pytestKeyless by default (DuckDuckGo's HTML endpoint) — no signup needed to try it. That
endpoint is meant for browsers, not scrapers, and can rate-limit or change markup
without notice; the parser lives in one function
(tools/search.py) precisely so a markup
change is a one-function fix.
No answer synthesis that smooths over disagreement into one confident paragraph — the report shows claims and their sources, and surfaces conflicts, rather than picking a side for you. No multi-provider model support yet (Anthropic only) — one well-tested path beats three half-tested ones. No browser automation — search results and their snippets are what gets read; nothing renders or scrapes full pages today.
MIT.