Descartes
README.md
# Descartes
> **doubt, until what remains is certain.**
[](https://github.com/pranjalbhatia710/descartes-mcp/actions/workflows/ci.yml)
> ### ๐ญ Sibling project: **[The Asker](https://github.com/pranjalbhatia710/the-asker)** โ *today's AI answers, this one asks.*
> The Asker learns to **ask** the sharpest questions to understand a subject;
> Descartes learns to **doubt** until what remains is certain. Two halves of one
> idea: the right question beats the confident answer.
> โ **https://github.com/pranjalbhatia710/the-asker**
๐ชถ **[Landing page โ](https://pranjalbhatia710.github.io/descartes-mcp/)** ยท `descartes --epigraph` for a passing thought from the man himself.
An MCP server that, on a prompt, **doubts every decision in a plan, doubts its own
doubts, and answers them โ grounding every answer in real evidence** โ iterating
until the plan stops producing new load-bearing doubts. Then it hands back a
refined plan plus the *few* decisions only you can make.
The point: no unexamined assumption reaches the work, so hallucination has no
room to hide.
---
## The loop
On a prompt, Descartes runs recursive Cartesian doubt:
1. **Draft** the plan / decisions.
2. **Doubt** every load-bearing decision (one of 11 doubt operators per doubt).
3. **Doubt the doubts** โ prune the trivial and manufactured ones.
4. **Answer** each surviving doubt *only from real evidence*:
- code doubts โ resolved from the `context` you pass in,
- world doubts โ resolved via **Exa** (cited, confidence-scored),
- ungroundable โ marked **NEEDS_HUMAN**.
5. **Doubt the answer** โ recurse: each settled answer is itself doubted (the
question behind the question), spawning the next layer of doubts, bounded by
depth and a global doubt budget.
6. **Bank it** โ every grounded finding accumulates into a **knowledge base**
that feeds the next round of doubting and the final plan.
7. **Revise** the plan with what survived, and **repeat** from step 2.
The output is therefore a **doubt tree** (each entry carries `depth` + `parent`),
not a flat list, plus a `knowledge_base` of accumulated facts.
**Stopping rule:** the loop stops the moment a full pass produces **no new
load-bearing doubt** โ that is convergence. It might be pass 3 or pass 9. **20 is
a hard ceiling, never a target**, and recursion is bounded by `DESCARTES_MAX_DEPTH`
(default 3) and `DESCARTES_DOUBT_BUDGET` (default 64). It never manufactures doubt
to keep going.
**Grounding rule:** every answer resolves against real evidence or becomes a
question for you. Low-confidence Exa is never asserted. 20 rounds of
self-answered, ungrounded doubt would be 20 rounds of confident hallucination โ
so that is forbidden by construction.
---
## Two engines (auto-selected)
| If you setโฆ | Descartes runsโฆ |
| --------------------- | -------------------------------------------------------------------------------------------- |
| `FIREWORKS_API_KEY` | a **panel** of distinct Fireworks model families. Agreement = settled; **disagreement = real uncertainty** โ the doubt is escalated. |
| *nothing* (in a client that supports MCP sampling) | **Claude alone** โ the client's own model runs the whole loop. |
| `OPENROUTER_API_KEY` | single-model "Claude alone" via OpenRouter (fallback when sampling is unavailable). |
| *no keys at all* | a **deterministic operator-template** fallback, so it never hard-fails. |
`EXA_API_KEY` is independent: set it to ground world doubts in any mode.
### No keys? It still works โ with just Claude
You do **not** need any API keys. Inside an MCP client that supports sampling
(Claude Code), the whole loop runs on **the client's own Claude model** โ code
doubts are resolved by reading the `context` you pass in, and anything that
needs an external fact or a human decision is handed back to you.
And if even sampling isn't available, Descartes **does not guess**. It will not
fake-confirm a doubt from keyword matching; it surfaces every load-bearing doubt
as a question in `needs_user` and tells you so. Every `doubt()` result carries an
`engine` and a plain-English `note` describing exactly how it was powered, so a
degraded run is never silent. The one rule it never breaks: *no answer without
evidence โ otherwise, ask you.*
---
## 3-step setup
```bash
# 1. install
git clone https://github.com/pranjalbhatia710/descartes-mcp && cd descartes-mcp
pip install -e .
# 2. (optional) bring your own keys โ copy names only, fill in your .env
cp .env.example .env # .env is gitignored; never commit it
# 3. run it (stdio MCP server)
descartes # or: descartes --selftest to prove the loop converges
descartes --demo # watch it doubt, live, in your terminal
```
### Watch it work
`descartes --demo` runs a scripted doubt session and renders it live โ a spinner
while it thinks, a self-drawing **doubt tree** (`โ` top-level, `โณ` recursion),
colour-coded statuses (โ confirmed ยท โ refuted ยท โ for you), the knowledge base
filling, and a `CONVERGED` banner.
Inside Claude Code, the real `doubt()` tool reports the *same* events to the
client as **MCP progress + log notifications**, so you see it working as it runs.
It never prints to stdout โ that channel is the MCP protocol. Set
`DESCARTES_VERBOSE=1` to also stream compact progress to the server's **stderr**
log.
### Add to Claude Code
Add this to your Claude Code MCP config (e.g. `.mcp.json` in your project, or via
`claude mcp add`). Keys live in your shell / config env โ **never in committed files**:
```json
{
"mcpServers": {
"descartes": {
"command": "descartes",
"args": [],
"env": {
"FIREWORKS_API_KEY": "${FIREWORKS_API_KEY}",
"EXA_API_KEY": "${EXA_API_KEY}",
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}"
}
}
}
}
```
Then ask Claude to *"use descartes to doubt this plan"* and pass the relevant
files as context.
---
## Tools
### `doubt(prompt, context="", max_passes=20)`
Runs the loop above. Returns:
```jsonc
{
"passes_used": 4,
"converged": true,
"plan": "<the refined, doubt-hardened plan>",
"doubt_log": [
{ "id": 1, "pass": 1, "depth": 0, "parent": null,
"operator": "assumption", "doubt": "...", "kind": "code",
"status": "CONFIRMED|REFUTED|UNKNOWN|NEEDS_HUMAN",
"resolution": "...", "source": "..." }
],
"doubt_tree": [ { "depth": 0, "children": [ { "depth": 1, "children": [] } ] } ],
"knowledge_base": [
{ "claim": "...", "verdict": "CONFIRMED", "evidence": "...", "source": "...", "depth": 0 }
],
"needs_user": [ "<the few decisions only you can make>" ],
"engine": "claude-sampling",
"note": "<plain-English: how this run was powered>",
"max_depth_reached": 2
}
```
`doubt_log` is the flat record (each entry stamped with `depth` + `parent`);
`doubt_tree` is the same data nested; `knowledge_base` is everything Descartes
grounded along the way, which it builds on instead of re-deriving.
`needs_user` is the product: it separates *what Descartes resolved itself* from
*what it genuinely needs you for*. Kept short โ the truly blocking ones only.
### `ground(query)`
Exa deep search โ cited, confidence-scored facts. Used inside the loop; callable
directly. Low confidence โ `status: "UNKNOWN"` (never asserted).
### `verdict(result)`
Pass a `doubt()` result. Returns `{ proceed, blocking_questions }`. `proceed` is
true only if the plan converged with no open load-bearing doubt; otherwise you
get the blocking questions.
---
## Doubt operators
Every doubt is tagged with one operator (shipped as data in
[`descartes/operators.py`](descartes/operators.py)):
`assumption` ยท `falsify` ยท `inversion` ยท `named_source` ยท `edge_case` ยท
`quantify` ยท `root_cause` ยท `reversibility` ยท `second_order` ยท `define` ยท
`deletion`
---
## Benchmarks โ how much better is doubting?
The honest metric isn't accuracy (that tracks whatever model you plug in). It is:
*of a plan's load-bearing assumptions, how many reach the work unexamined?* On a
seeded fixture of 6 real-shaped plans with **19 hand-labeled assumptions**
([`benchmark/coverage.py`](benchmark/coverage.py)):
| approach | examined | grounded from evidence | asked of you | reach work **unexamined** |
|---|---|---|---|---|
| ship the plan as-is | 0 (0%) | 0 | 0 | 19 (**100%**) |
| **Descartes** | 19 (100%) | 12 | 7 | 0 (**0%**) |
Unexamined assumptions reaching the work fall from **100% โ 0%**. Of the 19 it
surfaces, Descartes grounds **12 itself** and hands you only the **7** that
genuinely need a human. This is the conservative *floor* โ deterministic, in the
spirit of [The Asker](https://github.com/pranjalbhatia710/the-asker)'s synthetic
floor. A real model only widens the gap by catching assumptions the labels missed
and recursing deeper.
**Convergence** ([`benchmark/bench.py`](benchmark/bench.py)) โ it stops the moment
a pass adds no new doubt, and a runaway is capped:
| scenario | doubt depth | passes | converged |
| --- | --- | --- | --- |
| trivial | 0 | 1 | โ
|
| moderate | 3 | 4 | โ
|
| deep | 11 | 12 | โ
|
| spiral-guard | 40 | **20 (capped)** | โ by design |
Convergence is exactly `depth + 1` passes and never exceeds 20. Both benchmarks
run in CI on every push.
---
## Part of a bigger idea โ meet The Asker
Descartes has a sibling: **[The Asker](https://github.com/pranjalbhatia710/the-asker)**
โ *"Today's AI answers. This one asks."* An RL environment where an agent learns
to understand any subject by asking the sharpest questions, watching a field of
candidates collapse to the truth in as few questions as possible, and beating
frontier models that just guess.
They are two halves of one thesis: **the right question beats the confident
answer.** Guessing is where hallucination comes from; both tools replace it with
questioning.
- **The Asker** asks, to *understand* โ it learns, by reward alone, which
question removes the most uncertainty.
- **Descartes** doubts, to *be certain* โ it questions every decision, then
recursively questions its own answers, until nothing unexamined remains.
Point The Asker at a subject and watch it learn what to ask. Point Descartes at a
plan and watch it refuse to guess.
โ **[github.com/pranjalbhatia710/the-asker](https://github.com/pranjalbhatia710/the-asker)**
---
## Development & tests
```bash
pip install -e ".[dev]" # pytest + ruff
ruff check descartes/ tests/ benchmark/
pytest -q # tests, no network (all backends are mocked)
python -m benchmark.bench # convergence benchmark (see Benchmarks above)
python -m benchmark.coverage # coverage benchmark (see Benchmarks above)
```
The suite locks down the load-bearing invariants: the loop always terminates and
never exceeds the hard ceiling of 20, it converges as soon as a pass adds no new
doubt, it recurses (questioning its own answers) bounded by depth and a node
budget, nothing is asserted without evidence (ungroundable โ `NEEDS_HUMAN`,
low-confidence Exa โ `UNKNOWN`), `needs_user` is exactly the `NEEDS_HUMAN` doubts,
and panel disagreement escalates to the human. `tests/test_regressions.py` carries
one guard per bug found by the adversarial audit. CI (`.github/workflows/ci.yml`)
runs lint + tests + the self-test + both benchmarks on Python 3.10โ3.13.
## Safety
- **Bring your own keys.** Read from env only (`os.environ.get`), never stored,
never logged, never committed. `.env` is gitignored; `.env.example` has names
only.
- Every external call is wrapped in try/except + timeout + retry โ one failure
never kills a run.
- Exa results are cached and time-boxed so a pass stays fast; panel size is
capped on the live path (`DESCARTES_PANEL_SIZE`).
## License
MIT โ see [LICENSE](LICENSE).
TDQS
A3.7/5.0
Scored across 3 tools
Disambiguation5/5
Each tool serves a distinct role: doubt handles the iterative planning and doubt resolution, ground performs factual retrieval, and verdict gates the output. No overlap in functionality.
Naming Consistency3/5
Names are single words but mix verb and noun forms ('ground' as verb, 'verdict' as noun) without a consistent pattern like verb_noun.
Tool Count4/5
Three tools is minimal but well-scoped for the plan hardening domain, covering the main loop, evidence gathering, and output gating without apparent bloat.
Completeness4/5
The tool set covers the core workflow: iterative doubt resolution (doubt), external fact retrieval (ground), and a validation gate (verdict). Minor gaps like plan editing are absent but the core loop is complete.
Maintenance
ActivityStale
ResponsivenessNo issues