Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

triangulate-agent

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

Why this exists

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:

  1. 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.
  2. 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.
  3. 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.

Install

uv venv && uv pip install -e '.[dev]'
export ANTHROPIC_API_KEY=...

Use it

triangulate "What caused the 2010 flash crash?"

Watch it work — the dashboard

triangulate-dashboard          # -> http://localhost:8420

A 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.

Tracing & the eval gate

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).

What's deterministic vs. what needs a model

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.

Tests

uv run pytest

Search

Keyless 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.

What this deliberately doesn't do

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.

About

A research agent that plans multi-step investigations, tracks claims to their sources, scores credibility, and surfaces contradictions instead of picking a side.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages