How it works

Five stages.
Every one swappable.

Quire is a pipeline, not a framework. Each stage is an entry point you can replace without forking, and the default for each is the boring choice that survived benchmarking.

1 · parseFiles become spans with structure intact. Tables stay tables, footnotes stay attached to their claim, headings become hierarchy.
2 · indexSpans get embedded and written to SQLite alongside a BM25 table and a document graph. Incremental: a changed file re-indexes in milliseconds.
3 · retrieveHybrid dense plus lexical, fused with reciprocal rank, then reranked. Recency and supersession applied here, not in the prompt.
4 · generateOnly the selected spans go to the model, with a fixed instruction to cite by span id. Nothing else about your corpus is sent.
5 · verifyClaims decomposed and checked against their cited spans. Unsupported ones are cut before you see the answer.
01 — parse

The stage nobody writes blog posts about.

Roughly 60% of the parsing code exists because of PDFs, and most of the rest exists because of spreadsheets where the real data starts at row 14.

  • Layout-aware PDF extraction: merged cells, multi-row headers, rotated tables
  • OCR fallback for scans and screenshots, via Tesseract or a vision model
  • Markdown, HTML, docx, xlsx, pptx, epub, mbox, plus 30 community formats
  • Code-aware chunking that respects function and class boundaries
  • Parsers register through quire.parsers entry points. Adding one is a small file
custom parser
# pyproject.toml of your plugin
[project.entry-points."quire.parsers"]
jupyter = "my_plugin:NotebookParser"

# my_plugin.py
class NotebookParser(Parser):
    extensions = {".ipynb"}

    def parse(self, path) -> Iterable[Span]:
        nb = json.loads(path.read_text())
        for i, cell in enumerate(nb["cells"]):
            yield Span(
                text="".join(cell["source"]),
                locator=f"cell {i}",
                kind=cell["cell_type"],
            )

Why hybrid, still

Pure vector search fails on exact tokens: error codes, ticket numbers, a person's surname. Pure lexical fails on paraphrase. Fusing them costs about 40ms and removes an entire class of "why didn't it find that" issues.

Supersession is a retrieval problem

Quire builds a document graph from links, filenames, frontmatter and git history, so a policy that was replaced ranks below the one replacing it, unless you ask a question about history, detected separately.

02–03 — index & retrieve

SQLite until it hurts.

The default store is a single file next to your documents. It handles about 500k documents on a laptop, more than most people have, and it means there's no service to stand up before you can try the tool.

  • Dense vectors plus BM25, fused with reciprocal rank fusion
  • Cross-encoder rerank on the top 50, locally by default
  • Document graph for supersession, authorship and thread context
  • Postgres with pgvector for anything larger: one config line
  • Index format is documented and stable; it's just SQL you can query
04–05 — generate & verify

Generation writes. Verification decides.

These want different objectives, so Quire runs them as different models. A big model drafts; a small cheap one checks each claim against its span and never sees the question. Given the question it starts reasoning about plausibility instead of support.

  • Claims decomposed to atomic statements before checking
  • Verifier sees the span and the claim, nothing else
  • Explicit numeric rules: "roughly 340" entails "340" one way only
  • Conflicts surfaced, not resolved: you get both numbers
  • Every stage logged to a trace you can replay with quire trace
verification loop
for claim in decompose(draft):
    span = citations[claim.cite_id]
    if not entails(span, claim):
        draft.drop(claim)        # quietly, always

if not draft.claims:
    raise InsufficientEvidence(
        nearest=retriever.top(3),
    )

What this buys, measured

MetricOffOn
Claim attributable to its span71%99.2%
Correct refusal when absent12%94%
Recall on answerable questions100%98.8%
Added latency+180ms

Fictional figures, illustrating the tradeoff: verification costs a little recall and buys a lot of trust.

Agents

A cron job with a good memory.

An agent is a YAML file: an instruction, a schedule or trigger, and somewhere to put the output. No canvas, no node graph, no orchestration runtime.

.quire/agents/weekly.yml
name: weekly-changes
schedule: "0 7 * * 1"
ask: |
  What changed in the RFCs in the last 7 days?
  Cite everything. Max 400 words.
sources: ["docs/rfc/**"]
output:
  to: "stdout"     # or file, webhook, slack
strict: true        # no output beats a wrong one

Agents never act on their own

The only outputs are stdout, a file, a webhook you configured, or an exit code. There is no browser, no shell access, no "do the thing" step. If you want it to open a pull request, you pipe it into something that does, visibly, in your own CI.

They run where you run them

No scheduler service, no hosted runner. quire agent run inside a GitHub Action, a systemd timer, or a laptop crontab. The state is a file in .quire/.

Read the source instead.

The pipeline is about 4,000 lines. Everything on this page lives in quire/pipeline/, and the benchmarks justifying each default are in bench/.