Module references
Lens — retrieval & the answer policy
Lens is the module that turns a question into an answer you can check. It is one of eight capability modules and it is adoptable on its own: two React components, two primary HTTP endpoints, and three read-only observability endpoints.
What it does
Lens searches a workspace's documents and answers questions from them. Every answer comes back with its evidence attached: the atomic claims the model made, the [Sn] sources each claim cites, a retrieval-confidence summary, and an answer-policy verdict. The policy is the part that makes the output trustworthy — before generating it can decide to ask a clarifying question or abstain with pointers to the closest documents, and after generating it attaches a calibrated confidence and caveats rather than rewriting the model's prose.
The raw retrieval channel is also exposed on its own. GET /api/v1/search ranks passages with no model invoked at all, which makes it usable in a workspace that has no chat model configured.
Use it standalone
Adopting Lens alone means one package entry, one API scope, and a workspace that has documents in it. Nothing else in the eight-module surface has to be wired up.
Issue a key with the right scope
POST /api/v1/queryrequires theagents:runscope — the call invokes the workspace's chat model, so it gates like an agent run, and that scope is also what licenses the per-callpolicyModeoverride.GET /api/v1/searchrequireskb:read. A key with both covers the whole module. The three read-only surfaces below acceptkb:reador the legacyruns:read.Install only what this module needs
@eli-ai/react/queryimports@eli-ai/client/query,@eli-ai/contracts/query, and the shared provider/hooks. No other capability subpath is pulled in.installbash Wire the provider and drop in the component
EliProviderhas no API-key prop by design. It takes an already-built transport, so credential handling stays outside the render tree — in the browser, point the transport'sfetchat a same-origin route of yours that injects the Bearer key server-side.
What Lens does NOT require
No other module. With includeData left at its default (false) Lens never touches a data connector. Entity detection and graph fusion each degrade to a no-op when the graph is empty — detection failures are swallowed and retrieval simply proceeds without them. No golden set, connector, report, or agent is involved.
No embeddings provider. The vector channel degrades to absence on any failure (unconfigured provider, retired space, network error); full-text alone still answers.
No reranker key. The reranker setting resolves to a no-op passthrough when no provider key is present.
No Next.js. The React package uses ordinary React, URLs, callbacks, and the injected transport.
It does require documents in the workspace, and POST /api/v1/query requires a configured chat model — without one it returns 409 no-model-configured. GET /api/v1/search has no model dependency at all.
React components
@eli-ai/react/query exports exactly two components. Both render into the package's scoped eli-* classes and set eli-root on the outer element, so the optional @eli-ai/react/styles.css export styles them without leaking into your own CSS. Both are client components and must be rendered inside an EliProvider.
EliQuery
A single surface with two tabs. In search mode it renders a list of ranked passages — document title, path, snippet, a percentage badge and a relevance meter, each row a button that fires onDocumentSelect. In ask mode it renders the answer text, a badge for the policy verdict, a verified/total grounded badge when groundedness was computed, and the list of sources. Errors render an inline retry button. A search that matched nothing renders an empty-state card; ask mode renders a placeholder card until the first answer arrives.
EliQuery props
EliSearch
A convenience wrapper that renders EliQuery with allowAsk={false} and initialMode="search" pinned. It accepts every EliQuery prop except those two, so it is the component to reach for when you want retrieval with no model spend and no way for a user to trigger one.
Props the components deliberately do not expose
EliQuery calls ask({ question }) and search({ q, limit }) — nothing more. That means includeData, maxSources and policyMode on the query body, and rerank on search, are not reachable through these components. Use the headless createQueryClient with useEliMutation from @eli-ai/react/hooks when you need them, or set the workspace-level retrieval and policy defaults.
HTTP API
Five routes. The first two are the module; the last three are its read-only observability surface. Every request carries Authorization: Bearer …, plus X-Workspace-Id when the key is a multi-workspace user key. The apex domain 308-redirects, so non-GET curl examples pass -L.
/api/v1/queryscope: agents:runRun the full Lens pipeline — retrieval, entity detection, the policy pre-gate, one chat-model generation, a best-effort groundedness check, and the post-gate — and return a single structured answer. User keys additionally need the agents:execute capability. Returns 409 with the error code no-model-configured when the workspace has no chat model configured.
Request body
Reading the answer
Four fields on the response are nullable and mean something specific when they are null:
retrievalisnullwhen nothing was retrieved at all. Otherwise it is the fused summary:topScore,meanScore,chunkCount, andchannels, aRecord<string, number>keyed by channel name (fts,vector).groundednessisnullwhen no judge model resolved or the check failed. It is a two-field shape and nothing more:{ verified, total }, both integers — counts of claims the judge could verify against the evidence.policyisnullwhen the effective mode isoff, or when a gate failed (a policy fault never fails the answer). Theverdictis one of six values —proceed,clarify,abstain_with_pointersfrom the pre-gate;answer,answer_with_caveat,abstainfrom the post-gate. On a pre-gate short-circuit the block also carriesclarifyandpointers.usageisnullonly on a served cache hit. A pre-gate short-circuit reports zero tokens rather than null, because no model ran.
retrievalDebug is optional and purely informational — it never changes the sources or the verdict. See Retrieval pipeline for what each stage count means, and Answer policy for how confidence is computed.
/api/v1/searchscope: kb:readHybrid retrieval — full-text and pgvector, reciprocally fused — over the workspace's chunks. No model is invoked. User keys additionally need the documents:read capability. An empty or absent q returns an empty result set rather than an error.
Query parameters
Two details the shape hides
snippet is the chunk content truncated to 240 characters with an ellipsis appended — it is not a highlighted extract. rerankScore is omitted entirely (not null) when reranking did not run, which is why the contract types it as optional.
/api/v1/policy/decisionsscope: kb:read or runs:readThe append-only answer-policy decision log, newest first. Read-only — rows are written by the answer paths, never through this API. User keys additionally need the governance:read capability.
Query parameters
/api/v1/policy/statsscope: kb:read or runs:readDecision counts by verdict and by surface over the last 30 days — the rollout dial for moving a workspace from shadow to enforce. Same gate as the decision log.
/api/v1/cache/statsscope: kb:read or runs:readHealth of the semantic answer cache that sits in front of the /query path. Read-only; entries are written by the answer path and invalidated by document changes. User keys additionally need the documents:read capability.
hitRate is an estimate, and the client for these three lives elsewhere
hitRate is computed as totalHits / (totalHits + total) — each stored entry stands in for at least one prior miss. It is null when there is no data.
These three read surfaces are not on @eli-ai/client/query. They live on createQualityClient from @eli-ai/client/quality as listPolicyDecisions, getPolicyStats and getCacheStats, and the React surface for them is EliQuality in @eli-ai/react/quality — a component shared with Crucible, whose eval tabs you get whether or not you want them.
Data elements
Every table below is tenant-scoped: each carries a workspace_id column and a row-level-security policy that constrains both reads and writes to the workspace of the surrounding transaction. Lens never selects across workspaces, and a key resolved for workspace A cannot read workspace B's rows even through a crafted id.
| Table | Lens | Columns that matter | What a row means |
|---|---|---|---|
| documents | read | id · path · title · verification · acl_visibility · acl_principals · deleted_at | One source document. Lens filters on deleted_at and the two ACL columns before ranking, and reads verification into the policy's authority features. |
| chunks | read | id · doc_id · ord · heading_path · content · content_hash · token_count | The retrievable unit. The full-text channel searches a generated tsvector column on this table; content becomes the [Sn] snippet. |
| chunk_embeddings | read | chunk_id · space_id · embedding | The pgvector cosine channel. Absent or unconfigured, retrieval degrades to full-text only rather than failing. |
| embedding_spaces | read | id · provider · model_id · dim · status | Which embedding model the vector channel queries. At most one row per workspace has status 'active'. |
| entities | read | id · authority · lifecycle_status | Governance labels for entities detected in the question. Rows in draft or pending_review are dropped from the answer; a certified entity feeds the policy's hasCertifiedConcept feature. |
| policy_calibrations | read | version · coeffs · active | The fitted confidence coefficients the gates score with. At most one active row per workspace; a missing or unreadable row falls back to the built-in bootstrap defaults. |
| policy_decisions | write · read | id · surface · question · stage · verdict · mode · confidence · calibration_version · features · reasons · caveats · created_at | One append-only row per gate decision — a pre row and, when generation ran, a post row. surface is 'structured' for /api/v1/query. This is what GET /policy/decisions and /policy/stats read. |
| evidence_conflicts | write | claim_hash · claim_text · chunk_a · chunk_b · doc_a · doc_b · kind · stance_a · stance_b · detected_by | A detected disagreement between two cited chunks, deduped on the canonically ordered chunk pair. Written only when conflict detection is on and a judge model resolves. |
| semantic_cache_entries | write · read | exact_key · scope_key · question · embedding · answer · model_id · prompt_version · hit_count · expires_at · invalidated_at | A replayable StructuredAnswer minus usage, keyed by question + model + prompt version + effective config. Off by default; never written for includeData answers. |
| semantic_cache_dependencies | write | entry_id · doc_id · doc_content_hash | The [Sn] documents a cached answer depends on, with the content hash at store time — the lookup that invalidates an entry when a source document changes. |
The two rows you will actually query
policy_decisions is the audit artifact: it answers "why did the system decline this question?" with the exact feature snapshot the gate scored, and it is written in shadow mode too — which is what lets you measure a policy before you let it change any answer. semantic_cache_entries is the cost artifact: it is off by default, and answers with includeData: true are never cached because their [Dn] rows are non-deterministic.
How it composes
Lens is the module the other seven point at. It works alone, and each neighbor makes it measurably better at something specific.
What it gains
- Intake— the honest dependency. Lens ranks and cites documents; something has to put them there. Intake's connectors also populate
acl_visibilityandacl_principals, which is what lets restricted documents be retrievable by the right people and invisible to everyone else. Without Intake you can still write documents through any other path — Lens does not care how they arrived. - Atlas — the entity graph turns on two things. Graph fusion pulls in chunks that are structurally related but textually distant, reported as
retrievalDebug.stages.graphAdded. And question-entity detection populates theentitiesarray on the answer. With no graph, both degrade silently to nothing and retrieval proceeds on text alone. - Warrant— governance is what the policy's confidence is partly made of. Document
verificationfeeds the authority features (an answer resting only on unverified sources is capped), entitylifecycle_statuskeeps draft and pending-review concepts out of answers entirely, and Warrant's calibration surface writes thepolicy_calibrationsrow the gates read. Without it every document isunverifiedand the gates score against bootstrap coefficients — the policy still runs, just less sharply. - Conduit — genuinely required for one feature.
includeData: trueexecutes the data bindings of detected entities and adds[Dn]citations to the answer. With no connectors configured the call does not fail: the data layer is caught,dataCallscomes back empty, and the doc-grounded answer ships. Live data also has one policy consequence — anabstain_with_pointerspre-verdict is overridden toproceedwhen data actually ran, because rows can ground an answer that documents could not. - Lineage — Lens hands you
docIdandchunkIdon every source. Lineage is what turns those ids into a provenance trail and a blast radius. Without it the citation is still a real, resolvable document reference. - Crucible — the only way to know whether a retrieval or policy change helped. Because the two non-deterministic stages both have deterministic-off paths, an eval run can pin Lens to a fully reproducible configuration. Crucible also owns
/api/v1/evalsand/api/v1/qrels, which are not part of Lens. - Ports — the same pipeline is reachable over MCP as the
eli_querytool, so an external agent gets identical grounding and the identical policy verdict without going through HTTP.
What still works with none of them
A workspace with documents and a chat model gets the complete Lens contract: fused full-text retrieval, cited claims, a retrieval-confidence summary, groundedness when a judge model is configured, and every policy verdict including clarify and abstain. The parts that need neighbors — graph fusion, live data, verification-weighted confidence, fitted calibration — are the parts that were designed to be absent, and each one is a no-op rather than an error when it is.
Going deeper
Retrieval pipeline explains the five stages and how each degrades. Answer policy covers the verdict router and the confidence model. Tune retrieval quality and Tune the answer policy are the operator loops. Structured query is the endpoint reference, and JavaScript & React SDK covers the transport, workspace selection, and error types shared by every module.
Extending Lens
rerankers adds a reranker kind next to Voyage, Cohere and the self-hosted ONNX model (the host owns the reported kind, so a plugin cannot impersonate voyage in the trace), and candidateChannelsmerges an extra recall channel into the candidate pool under an 800 ms budget. A contributed channel is ACL-filtered downstream like every other channel, so the worst it can do is promote documents the caller could already read. Lens emits answer.produced and cache.invalidated. See the extension model for the contract and the trust boundary, and Build a plugin for a worked example.