construe

Documentation

Everything an integration needs.

Construe is in a closed beta. These pages describe the shipped interface — the element, the endpoint you host, and the object you get back — and are the same document the console links to.

What Construe is#

Construe turns what a person types into a constraint object: a small structure your existing index can run. It is not a query language, not a plan, and not a model call.

Annotation is a lookup against a compiled vocabulary; interpretation is a pure function in an isolate. A parse is a few milliseconds of CPU and zero tokens, which is what makes a flat subscription honest and what makes the latency the same on the thousandth request as on the first.

There are two kinds of workflow. A search workflow takes a sentence and answers a constraint object. An extraction workflow takes a document — a photograph, a scan, a PDF — and answers an object conforming to a shape you declared.

Quickstart#

One script, one tag, one event. The element owns the session, the socket, the debounce, the autocomplete and every way this can fail.

<script type="module" src="/construe.js"></script>

<construe-search src="/api/construe-bootstrap"></construe-search>

<script>
  document.querySelector('construe-search')
    .addEventListener('construe-submit', (e) => {
      // e.detail.constraint is the structure. Run it against your index.
      // It is null when the parse failed — search the raw prompt instead.
      runSearch(e.detail.constraint ?? { text: e.detail.prompt });
    });
</script>

src points at your server, not ours. That endpoint is the only thing you have to write.

The bootstrap endpoint#

Your backend mints a session with your API key and hands the result to the browser. It is the only place that can decide anything per visitor — which workflow they get, whether they are allowed to search at all — so the decision lives with you rather than in markup somebody has to redeploy.

Call POST /v1/search-token with your key and forward the whole response:

// your server
const res = await fetch('https://construe.3sln.com/v1/search-token', {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.CONSTRUE_KEY}`,
    'content-type': 'application/json',
  },
  body: JSON.stringify({ workflow: 'wf_...' }),
});
return new Response(await res.text(), {
  headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
});

The response is a configuration document, not just a credential:

credential
A short-lived signed session token. Fifteen minutes, and it cannot be revoked — which is why it is short.
session
The session id. The receipt chain binds to it, so a bootstrap without one gives a socket that can never resume.
socket
Absolute URL of the door this credential may open. Never construct it yourself: a search session and an upload session open different doors.
grant
What this session may spend, so a client can size its own behaviour rather than discovering the bound by hitting it.
workflow, version
Which workflow, and which version this session will be answered by — resolved, so a caller asking for ^3.1 knows they got 3.4.
config
Settings for the element: the vocabulary scope autocomplete draws from, the URL of the autocomplete corpus, and the ingress ceiling for uploads.

Never send an API key to a browser. The element refuses any credential whose signed typ is not a session token — not as a warning, as a refusal. A page with a working search box and a leaked management key in devtools is worse than a page with a broken search box, because only one of those gets fixed today.

Serve it same-origin. The element sends credentials: 'same-origin' so your endpoint sees the visitor's login cookie; point src at another origin and the cookie does not ride along, so your auth check sees an anonymous request and says yes to everybody.

The constraint object#

Four keys. Your index already has three of them.

excludes
A flat list of atoms. Match none of them. It is the closure — if the person excluded nuts, every atom that has a nut is already in this list, so your index does no traversal.
requires
Groups. Any within a group, all across groups. That is what turns "chicken or turkey, and under 30 minutes" into two groups rather than one ambiguous list.
authored
What the person actually typed, as spans. Render this one — it is what lets you show them their own sentence with the parts you understood marked.
terms
Every written form of every atom, in every locale the workflow serves. For a title match or a highlight.

The response also carries coverage — the fraction of the sentence that was consumed — and unconsumed, the words that were not. Both are ordinary: a few unmatched words in a long sentence is a working parse, and the unmatched words are the vocabulary telling you what it is missing.

Diagnostics are opt-in. The lean answer is what every caller gets; pass the extra parameter when you want the annotation spans and the shape, and pay for them then.

Two edges, because one edge forces you to lie

The vocabulary stores containment in both directions. Cashew butter is a spread and has a tree nut. Excluding nuts has to catch the jar; requiring nuts must not return it. A single "related to" edge cannot express that, and a taxonomy that only has one makes somebody tag the jar twice and then keep the two tags in step forever.

Element events#

Every event bubbles and is composed, so you can listen on an ancestor. Every one carries a detail.

EventWhenDetail
construe-parseA parse came backThe whole response: constraint, coverage, unconsumed, terms
construe-submitThe person pressed Enter or accepted a suggestionprompt, constraint (null on a failed parse), and resolve()
construe-errorAnything failedcode, message, phase, retryable, retryAfter
construe-extractionA document was readstate, object, provenance, coverage
construe-progressAn upload movedsent, size, phase
construe-uploadBefore a file is read — cancelablename, size, mediaType

A failed parse must never eat the Enter key. construe-submit fires whether or not the parse worked, and constraint is null when it did not. Reading resolve is how you tell the element you are going to do more work, so its spinner stays yours until you say otherwise.

The elements expose ::part(field), ::part(listbox), ::part(row), ::part(zone) and others, plus custom states :state(busy), :state(open), :state(invalid) and :state(dragging). Colours come from --construe-* custom properties. That is the whole styling surface, and it is versioned.

Extracting from documents#

<construe-extract> is the same shape of integration: one tag, one attribute pointed at a bootstrap endpoint that mints an extraction session.

<construe-extract src="/api/construe-upload"></construe-extract>

<script>
  document.querySelector('construe-extract')
    .addEventListener('construe-extraction', (e) => {
      if (e.detail.state === 'ready') save(e.detail.object);
    });
</script>

Your endpoint calls the same mint with { endpoint: 'extract' }. One route mints both kinds of session, and the workflow's kind is not readable on the mint path — so a client that omits this gets a search session and is refused at the upgrade, with a message about a credential rather than about the mistake.

Behind the element, a document goes up over a WebSocket in mebibyte frames, assembled as a multipart upload. A dropped connection resumes from the last flushed part rather than from the beginning; the digest is computed on both ends, and a mismatch is refused rather than extracted. Extraction is then polled on the same connection, because a four-hundred-page scan is minutes of OCR and a poll is a frame rather than a request.

The result carries four things worth knowing about:

state
ready, refused, failed or pending. None of them is an error: a refusal is the extractor declining to stand behind an answer, which is information.
object
The extracted object, conforming to the shape the workflow declares.
provenance
For each field, which page and which text it was read from.
coverage
How much of the document was consumed.

A shape can declare relationships between its own fields — that a total sums its lines, for example. A wrong extraction is then provable without asking a model anything, and every case that catches is a case nobody has to be paid to judge.

Sessions, grants and receipts#

A session is the unit. It is minted by your server, it carries a grant, and it spends that grant over a WebSocket. Nothing on the message path reads a database or a shared counter, which is why the tenth search of a session costs the same as the first.

SessionGrant
Search20 searches, 500 suggests
Extraction8 documents, 1 GiB, and the frames to carry them

A credential lives fifteen minutes. When a connection closes cleanly the server hands back a receipt: the unspent balance, signed, redeemable by exactly one later connection on the same session. That is what lets a session survive a flaky network without a shared counter anywhere — and a client that drops the socket instead of saying goodbye gets re-minted at a quarter of its grant, which is the honest thing to do when nobody can establish what was left.

The elements handle all of this. It is described because you can see it on the wire.

Limits#

WhatBoundWhy
Prompt280 charactersPast that it is not a sentence somebody typed into a search box
Document64 MiBThe ceiling travels on the config document, so the element refuses before a byte moves
Session credential15 minutesIt cannot be revoked, so it is short
Concurrent uploadsOne per sessionA transfer session carries one open transfer

Rate ceilings are abuse ceilings, not commercial limits. A plan buys a pace, not an allowance: nothing is deducted and nothing runs out. A team ahead of its pace is slowed and still served, and a paying plan never receives a quota refusal.

Errors#

Every failure carries the same four things: a code you can switch on, a phase saying which half of the flow it happened in, whether it is retryable, and — when the service said so — how long to wait. retryable is stated, never inferred: a frame has no status code to fall back on, so a client that guessed from the code string would be maintaining a copy of our taxonomy.

CodeMeans
malformedThe request is not one
unauthenticatedThe credential is not valid
no_teamSigned in, and on no team yet — a console should offer to create one, not sign the person out
forbiddenThe credential is valid and does not grant this
conflictThe write disagrees with what is already there
too_largeOver a declared bound
rate_limitedA burst against a window that reopens in seconds — retry
quota_exhaustedAn allowance, gone until the period rolls — do not retry
shedWe are shedding load. The demo tier is shed first, paid last

How the parser improves#

A workflow is evolved, not written. Traffic the parser got wrong — sentences with low coverage, words the vocabulary did not know — is sampled into a corpus. An agent proposes a change to the interpretation function or the vocabulary. Seven gates and a held-out suite decide whether it ships, and the suite is pinned at the start of a round so a proposal cannot be graded against a target it moved.

Nothing is promoted because it looked better. A round that improves the sampled cases and regresses the held-out suite is refused, and the account of why is kept with the version.

Sampling is opt-in per workflow and never on by default. Demo traffic is never sampled and never trains anything.

Where to go next#