@bevia/sdk
v0.5.0
Published
Bevia SDK — a thin client for a running Bevia Local engine, over its public doors only: submit evidence, read the governed current World (Matter, standing, condition history, relations, receipts by exact identity), subscribe to what happened, propose, and
Maintainers
Readme
@bevia/sdk
Bevia turns perception into governed operational understanding.
This SDK talks to a running Bevia Local engine — the governed intelligence runtime the owner runs. Send evidence in, read the governed current World out (Matter, standing, condition history, relations, receipts by exact identity), subscribe to what happened, propose, and report what you did. Zero dependencies. Node 18+, Deno, Bun, browsers.
your program ──intake.submit──▶ ┌──────────────────┐ ──world.* / attention──▶ your program
│ Bevia Local │ ──events.on───────────▶ your program
your program ──propose────────▶ │ governed World │
your program ──proposals.report │ (an HTTP URL) │
└──────────────────┘The engine never calls you; you connect to it. Nothing here reaches the engine's database; every call is a door the owner granted a capability for, and a refusal names the capability you lack.
What this SDK is — and is not
It is a thin client over the public doors of a running Bevia Local
engine: the HTTP protocol every Bevia client uses (v1.3, shipped as
PROTOCOL.md in every developer workspace and documented at
bevia.co/build). MIT, public, zero dependencies, so anyone can read every
line of what it sends.
It is not a way into the engine. The SDK carries:
- no engine code — nothing that admits, forms or publishes meaning;
- no database driver — it cannot open the engine's data, and no door returns a database handle;
- no owner-only route — revoking sensors, resolving proposals, clearing or importing a World, materializing the vault are the owner's, through the app, and are not in this package;
- no credentials — the model the owner configured is invoked by the engine
on
ask; your program never sees the owner's key.
Portability runs outward: a program can read the World, its publications and (with the owner's grant) export the whole substrate. Bringing a World in is the owner's act in the app, never an SDK call.
The package's publish step audits every shipped file — code, declarations, this README, the license and the manifest — for engine names, internal paths and anything but the doors the protocol publishes, and fails closed. If you can do something with this SDK that the protocol document does not describe, that is a bug — report it.
Install
npm install @bevia/sdkThat is the whole install. The package is on the public npm registry; nothing else — no checkout, no tarball, no private registry — is needed or offered. It speaks protocol v1.3.
No engine to hand? The Build on Bevia quick start at bevia.co/build shows how to run a sandbox engine with a small published World and a token that already works.
If you are a coding agent
Read AGENTS.md beside this README in the SDK's source (also at
bevia.co/build/AGENTS.md): the rules a program on Bevia follows, what a
caller may never declare, and how to validate what you built. The
step-by-step build loop is the bevia-app-builder skill in the same
source (skills/bevia-app-builder/SKILL.md, also at
bevia.co/build/skills/bevia-app-builder/SKILL.md). bevia.co/llms.txt
points at all three.
Connect
Open Bevia's menu-bar icon → Engine Controls… → Create a
developer workspace, once. It writes
~/Bevia/Developer/bevia-workspace.json with the engine's port and a
working token. Then:
import { connectLocal } from "@bevia/sdk";
const bevia = await connectLocal(); // reads the workspace file
console.log(await bevia.whoami()); // { name, kind, capabilities, pending }Or pair your own program with a code from the app (Settings → Sources) and ask for what it needs:
import { BeviaClient } from "@bevia/sdk";
const res = await fetch("http://127.0.0.1:PORT/pair", {
method: "POST",
body: JSON.stringify({ code: "ABCD2345", name: "Thesis Guard", kind: "extension",
capabilities: ["capture", "read", "subscribe", "propose"] }),
});
const { token } = await res.json(); // store it; it is shown once
const bevia = new BeviaClient({ url: "http://127.0.0.1:PORT", token });BEVIA_ENGINE_URL + BEVIA_TOKEN in the environment also work.
Where the engine is
The engine listens on loopback by default, so http://127.0.0.1:PORT is
where you will usually find it, and connectLocal() looks there. But the
address is just an address: pass any URL the engine is actually reachable
at — another machine on the owner's network, a tunnel the owner opened —
and everything below works unchanged.
const bevia = new BeviaClient({ url: "https://engine.example.internal", token });The doors, the capability grants and the refusals do not change with the hostname. Making an engine reachable beyond the machine it runs on is the owner's decision; the SDK neither arranges it nor assumes it away.
Evidence in
await bevia.intake.validate(capture); // dry run: writes nothing, says exactly what is wrong
await bevia.intake.submit({
thread_id: "garmin-runs-2026-07-15", // stable: re-sending upserts
source_platform: "garmin",
source_kind: "garmin_activity",
conversation: [{ speaker: "garmin", text: "6.2 mi in 52:10, avg HR 148.", emitted_at: "2026-07-15T13:58:00Z" }],
});
// Where it happened (posemesh-first) rides on the capture:
await bevia.intake.submit({
thread_id: "cactus:dom-store-142:coca cola",
source_platform: "cactus", source_kind: "cactus_search",
conversation: [{ speaker: "robot", text: "Located Coca-Cola 12oz (SKU 4921) for “coca cola”", emitted_at: at }],
location: { provider: "posemesh", domain_id: "dom-store-142", label: "Whole Foods — SoMa", pose: { x: 12.4, y: 0, z: -3.1 } },
}, { navmesh: { x: 12.1, z: -3.0, yaw: 1.57 } }); // robot-action data rides the envelope, never the labelSend what happened, verbatim, with the time it happened. The engine decides what it means; that is the whole contract.
What you can never send
evidence_shape (how evidence is shaped is declared engine-side per
source_kind) and the authority fields — source_object,
source_state, source_attestation, standing, scoring_basis,
establishment_basis, role_basis, authority, trusted — at any
level: the capture, a turn, a turn's metadata, or the envelope. A
remote program carries raw source evidence; the engine's trusted adapters
establish source authority. The engine ignores or verifies these anyway (a
forged attestation is refused and the capture lands with no source
object); the client refuses them before they leave, with the field named,
so you learn at the keyboard rather than weeks later as a silent zero in
corroboration. TypeScript refuses evidence_shape at compile time; the
runtime check covers the rest and callers who are not compiling.
The envelope is read
intake.submit(capture, envelope) carries the envelope to every landed
row's metadata verbatim (capture_envelope). It is not inert: the engine
reads watched_root and origin_remote from it when it resolves which
project a row was filed under. Send only facts the source established.
The Local door does not validate the envelope's shape; the Cloud door
does. It can never carry source authority.
Structured records
A turn can carry the source's own typed fields beside the rendered text.
Send them in record — nested objects and arrays survive as sent, and
numbers stay numbers:
await bevia.intake.submit({
thread_id: "pos:store-142:2026-07-15",
source_platform: "square", source_kind: "pos_sale",
session_facts: { started_at: "2026-07-15T08:00:00-07:00", total_turns: 412 },
conversation: [{
speaker: "register-3",
text: "Sale 4412 — 3 items, $27.40, card",
emitted_at: "2026-07-15T13:58:00-07:00", // the source's event time
upstream_id: "sq_txn_4412", // the source's own id for the record
source_appended_at: "2026-07-15T14:02:11-07:00",
record: {
id: "4412", total: 27.4, currency: "USD", tender: "card",
items: [{ sku: "4921", qty: 2, unit: 3.5 }, { sku: "1180", qty: 1, unit: 20.4 }],
},
}],
});Four things to know, all of them deliberate:
recordis faithful source content, never authority. Putting a field there does not make it true, does not classify the turn, and does not let a payload claim standing it was not granted. Which of its fields mean anything is declared by the engine for the capture'ssource_kind; this layer reads none of them.- JSON only, 32 KiB. A plain JSON object that survives
JSON.stringify→JSON.parseunchanged (Date,Map,BigInt, class instances: convert them yourself, so the conversion is yours and visible). Over the cap the record is dropped, never truncated — a cut record is a different record — and the turn still lands without it. It is temporary working material on the same retention floor as the raw text; nothing durable should assume it is still there. upstream_idis the source record's identity;thread_idis not.upstream_idis the citation key — what lets a later reader point at the exact original row, message or event.thread_idnames the stream, and the engine derives its own de-duplication key per turn.emitted_atis event time;source_appended_atis not.emitted_atis when the thing happened on the source's clock and is the only event-time candidate the engine accepts.source_appended_atis when the source wrote the row, kept as a disclosed fact and deliberately never read as event time. Sending it does not fill in a missingemitted_at; nothing does. Leaveemitted_atabsent when the source has no true event time and the engine will say so honestly rather than guess.
session_facts are the source's own facts about the session as a whole.
started_at / ended_at time the session, never a turn inside it,
and will not stand in for a turn's missing emitted_at.
Validate first
intake.validate runs the same shape check, the same normalizer and the
same record sanitizer as a real capture, and writes nothing. It is the
only place that tells you about dropped records, because a dropped
record does not fail a capture — the turn lands without it:
const dry = await bevia.intake.validate(capture);
// {
// valid: true, would_create: 3, skipped: 0, errors: [],
// records: { present: 3, preserved: 2, max_bytes: 32768,
// dropped: [{ turn_index: 1, reason: "over_cap", bytes: 41231 }] },
// note: "dry run — nothing was written. …"
// }
if (dry.records?.dropped.length) throw new Error("records would not survive");valid: false means the payload could not be understood at all. Check
records.dropped too — and skipped, which counts turns carrying no
usable text.
Building an adapter
An adapter reads a source and hands over what the source actually said. That is the whole job, and its shortness is the point: everything an adapter is responsible for is a fact it can transcribe, and nothing it is responsible for is a judgment it has to make.
import { defineAdapter } from "@bevia/sdk/adapter";
export const acmePos = defineAdapter({
manifest: {
schema: "bevia-source-contract/v1",
source: { kind: "acme.pos_sale", version: "1.0.0" },
adapter: { name: "acme-pos", version: "0.1.0" },
records: {
identity: "txn_id", // the source's own id
clock: { field: "closed_at", format: "iso8601" }, // the source's own clock
appended_at: "written_at",
sequence: "seq",
},
session: { identity_fields: ["shift_id"], close_markers: [{ field: "shift_closed", equals: true }] },
structure: { grouping_fields: ["register"], numeric_fields: [{ field: "total", fixed_unit: "USD" }] },
population: { mode: "finite" },
},
read: async (since) => ({ records: await readRegister(since), complete: true }),
});
const batch = await acmePos.read(since);
await acmePos.submit(bevia, batch); // any client, any runtime URLThe manifest declares only where a source puts its own native facts: which field is the record's id, which is its clock, which fields group it, whether the population ends. It is JSON and only JSON — no callbacks, no expressions, no query language, no addresses to fetch. There is nothing in it for a runtime to execute, because a manifest is read as data or it is refused.
What a manifest may not declare is any judgment: how evidence is shaped, what it means, what standing it has, how much it matters, what it relates to, which Territory it belongs in, whether it deserves Attention, whether it is ready to fold. Those are Bevia's, from the evidence, under its own law. The validator names each refusal rather than leaving it to a generic "unknown key", so you learn why.
One distinction that is easy to get backwards: the refusals apply to
manifest keys, never to the field paths a manifest points at. A
source is perfectly entitled to a column called importance, and you
may name it as a grouping field — pointing at a source's own field is
not a claim about what Bevia should do with it. Declaring importance
itself is what is refused.
Nothing about an adapter is loaded by Bevia. You write it, you distribute it, you run it; what crosses the boundary is JSON through the same door every other capture uses. Until the owner has activated a contract for it (next section), a custom source kind is carried faithfully and prepared by nothing: the evidence lands and is preserved, and no treatment is chosen for it.
Registering a custom source
Before Bevia will accept records from a source it has never heard of, the owner has to accept the structure you declared. You propose; they approve; then you submit.
// 1. You propose. Needs the `source_contract` capability.
const { receipt, next } = await acmePos.register(bevia);
receipt.status; // "proposed" — nothing is admitted yet
receipt.manifest_digest;
receipt.runtime_id; // the runtime this is true on
// 2. The owner activates it in the Bevia app. You cannot; the door is
// not in this SDK, on purpose.
// 3. Then submit against it.
await acmePos.submit(bevia, batch, { contract: receipt });Re-proposing the identical manifest is idempotent — the digest is the identity, so the same bytes are the same contract. Change the structure and you have a new one: two different structures may not share a source kind and version, because which structure did this row arrive under has to stay answerable. Bump the source version; that is what versions are for.
What registration means
Bevia accepts this as the declared structure of this source.
It is not an acceptance of your interpretation of the source.
That sentence travels in the receipt itself, in means, so a receipt
read later and out of context still says what it is. You may declare
that trade_id is the row's identity and trading_day is the session.
You may not declare what a trade means, what standing it has, what it
relates to, or what anyone should notice about it — those stay Bevia's,
decided from the evidence, and registering changes nothing about them.
Custom source kinds are namespaced
com.example.trade_feed, com.acme.inventory_sheet,
com.builder.robot_events. Built-in kinds carry no dot at all, so
requiring at least one makes impersonating one impossible by the shape
of the name, before any reserved list is consulted. bevia., local.,
system. and builtin. are reserved on top of that.
Every refusal, and what it means
A capture under a namespaced kind must reference a contract, and the runtime resolves that reference against its own registry — never trusting the payload. It refuses, by name:
| code | what happened |
|---|---|
| contract_required | a namespaced kind arrived with no contract reference |
| inline_manifest | the capture carried a manifest; register once, reference thereafter |
| unknown_contract | no such contract on this runtime |
| contract_inactive | proposed, not yet activated by the owner |
| contract_revoked | the owner closed this door |
| digest_mismatch | the manifest changed, so it is a different structure |
| source_kind_mismatch / source_version_mismatch | registered for something else |
| sensor_not_authorized | the owner did not authorize this client for it |
Provenance and revocation
Every admitted row is stamped with the contract's id, digest, kind, version, adapter and runtime — built from what the registry resolved, so structurally valid JSON can never mint a provenance fact it was not granted.
That stamp is why revocation is safe: it closes the door going forward and cannot reach backwards. Evidence already observed keeps its own answer to what declared structure did this arrive under, and an export years later still carries it. A later administrative act does not rewrite what was seen.
What is registered is a manifest, never code
Nothing is downloaded, imported, evaluated or fetched. Your adapter runs in your process and talks through the public doors like any other sensor. A manifest is JSON that is read, and the schema refuses everything that is not.
Qualifying an adapter
import { qualifyAdapter } from "@bevia/sdk/qualify";
const report = await qualifyAdapter(acmePos, {
batches: [{ records: dayOne, complete: true }],
expected: { record_ids: ["T1", "T2", "T3"] }, // what the source says should be there
late: [{ records: [arrivedLate] }],
});
report.ready_for_use; // nothing here proved it broken
report.blocking; // what to fix
report.undetermined; // what this sample could not decide — read this tooOffline, on your machine. It checks identity (present, unique, transcribed from the source rather than minted, stable under reordering and re-batching), clocks (parse, zone, event time kept apart from append time, sequence where declared), declared field-path coverage, population accounting, session boundaries, replay stability, rollover on source close markers, late arrivals, oversized records, and records that render nothing.
The rule that governs every verdict: what the sample cannot establish
is UNKNOWN, never PASS. A streaming source cannot prove its
population complete. A sample stating no expectations cannot prove its
grouping right. Reporting those as passes would turn an absence of
evidence into a clean bill — the one failure a qualification tool must
not have. ready_for_use says only that nothing here proved the adapter
broken; read undetermined before believing more.
Where this stops
There are two halves to knowing whether an adapter is any good, and only one belongs in public code.
| | | |---|---| | Structural — identity, clocks, replay and shuffle, grouping boundaries, completeness, late arrivals, rollover, dropped records | here, offline, arithmetic anyone can read | | Cognitive — atomization, smear, evidence coverage, citation quality, narrative sufficiency, useful preparation range | inside Bevia, against the real engine |
The second set are answers Bevia produces by running the evidence
through its own preparation and narration. Computing them here would
mean shipping that machinery. So they are decided in the app: you load a
sample, Bevia evaluates it, and you get the verdict and the diagnostics
— not the workings. The report names them under deferred rather than
faking a check or leaving them out.
In one line: you can inspect whether your adapter preserves the source correctly; Bevia evaluates whether that source can produce good governed meaning.
source_shape is the same boundary in one field. It stays
undetermined unless you pass a contract Bevia already issued, whose
digest matches your manifest. This library never infers it — how
evidence is shaped decides how it is prepared, and a tool you run on
your own machine is not where that is settled.
The World out
const read = await bevia.world.current(); // the governed CURRENT World, or null before the first publication
read.world.matter // current Matter — never a superseded row
read.world.relations // relations whose endpoints are current
read.world.references // source-established names for identities a current Matter holds
read.durable // the whole durable World, history included
read.transition // set when an older publication's snapshot was served
const why = await bevia.world.inspect(matterId); // establishing spans, standing history
const insights = await bevia.attention(); // what the publisher selected
const tracked = await bevia.followthrough(); // suggestions and their assessments
const goals = await bevia.objectives(); // see the note below — null todayHistory and present, together
The durable World a publication carries is history and present
together. When a Matter's basis is superseded, its row stays — with
current_standing: "superseded" and the lineage operation that moved it
onto successors the World also holds. So durable_world.matter.length is
not the size of the World, and a superseded row is not something the
owner currently holds. On one measured World: 149 durable rows, 74
current Matter, 75 historical. This has been so since 2026-09-18; a
program that counted matter before then has been over-counting since.
world.current() applies the same projection the product applies —
exported as currentWorld() if you hold a publication yourself:
- current Matter:
current_standinginestablished | candidate | open | unknown(unknownis current — the source carried no standing); - Matter-scoped carriers (
anchors,where,occurred_when,facets,claim_versions) follow their Matter; - relations keep only those whose endpoints are current; a namespaced
referent in a source's own identity space (
pos_txn:T142-0818-0243) is never a Matter and is always current; - references keep those whose subject a current Matter holds;
- history carriers cross verbatim and carry no contract in this SDK.
Reading a Matter
import { isCurrentMatter, isHeldMatter, conditionHistory, currentCondition, referencesOf } from "@bevia/sdk";
for (const m of read.world.matter) {
m.current_standing; // read this first — see Standing
m.claim; // null = a recorded unknown, not empty meaning
m.unknowns; // verbatim, never trimmed
m.provenance.origin; // lawfully null: the compiler is an instrument, not the origin
m.support_population; // corroboration and recurrence, as two numbers — never one score
if (isHeldMatter(m)) m.succession; // current, standing on a superseded basis under a typed hold
}Standing is established | candidate | open | rejected | superseded |
withdrawn | unknown. This library never infers one.
Condition history
A Matter's conditions[] holds one row per declaring record, never
edited. The current status of a condition identity is its latest
declaration by the declaration's own clock:
const rows = conditionHistory(m, "shelf_scan@v1#4"); // [11:30 open, 18:20 open, 23:58 resolved]
const now = currentCondition(m, "shelf_scan@v1#4"); // { status: "resolved", declared_when, record_id, upstream_id, … }
referencesOf(read.world, "condition:shelf_scan@v1#4"); // the names and ids sources gave this identityPackets that re-carried the same declaration are provenance
(carried_in), never additional history. A declaration with no clock
sorts after every clocked one and is never given a time from anywhere
else.
Evidence identity
Every receipt (evidence_refs[].ref) is an exact identity:
semantic_moment:<id> or raw_moment:<id>, optionally :span:<n>.
Follow it by identity or not at all — never by matching prose, never by
picking a neighbour.
import { parseEvidenceRef } from "@bevia/sdk";
parseEvidenceRef("semantic_moment:fc3d…"); // { carrier: "semantic_moment", id, span, carrier_basis: "declared" }
parseEvidenceRef("moment:fc3d…"); // null — the bare prefix is retired; pass the store the collection declares
parseEvidenceRef("moment:fc3d…", "semantic_moment"); // { …, carrier_basis: "bound_by_collection" }A proposal receipt's moment_id is a RAW moment (raw_moment:<id> in
this vocabulary).
Relations
for (const r of read.world.relations) {
r.subject; r.predicate; r.object; // endpoints: a Matter id, `condition:<id>`, or a namespaced referent
r.current_standing; r.declarant; r.evidence_refs;
r.reference; // present only when a source's own reference FIELD formed it
r.succession; // present only when an endpoint was superseded
}Predicates a source's own words may establish: corrects, contradicts,
supports. Predicates a source's own structured reference field may
establish, under a lane contract: returns_against — a POS return's
ref_receipt is the transaction it returns against. Such a relation
always carries reference:
r.reference = {
basis: "source_declared_structured_reference",
lane: "pos_tlog", reference_field: "ref_receipt", reference_value: "T142-0818-0149",
source_record_id, subject_identity: "pos_txn:T142-0818-0243", target_identity: "pos_txn:T142-0818-0149",
target_record_id: "…" | null,
target_resolution: "exact" | "absent" | "ambiguous",
contract_predicate: "returns_against", // always equals r.predicate
};It is established only on an exact target that names its record.
An absent or ambiguous target crosses as a relation of standing
unknown with a null target — explicitly unresolved, never bound to a
likely candidate (isResolvedReference(r) says which). The predicate is
the lane contract's: no caller names one, this SDK never mints one, and
REFERENCE_PREDICATES is closed. A namespaced endpoint answers
not-established from world.inspect — it is a source identity, not a
Matter, and that is the truthful answer.
Succession is not authority. When a relation's endpoint is
superseded, the relation continues onto a successor only when exactly one
successor carries the receipts the endpoint cited for it
(succession: { status: "re_elected", successor, declaring_refs }; the
predecessor row stays superseded, pointing forward). No successor
carries them → { status: "held", reason: "authority_not_carried" }.
Several do → { status: "held", reason: "ambiguous_successor" } — nothing
chosen, nothing duplicated. Withdrawn or revoked → no continuation.
Refusals at the read doors
/world-snapshot, /world-inspect, /world-diff and /followthrough
refuse at HTTP 200 with { world_snapshot_missing: true,
code, reason } (not_published, pointer_unreadable, stale_builder,
parse_failure, schema_unknown, builder_mismatch, wrong_principal);
isWorldSnapshotMissing() tells them apart. world.snapshot() returns
the raw artifact when you want the concept readings beside the durable
World; this SDK declares no contract for those.
territories() was removed in 0.4.0: the engine retired the
/territories door (HTTP 410) with the legacy geography it read. Regions
are a map drawn from the World, not part of it, so world.current() has
none; this SDK doesn't read Regions yet.
objectives()returnsnulltoday. It reads whatever the engine publishes atobjectives/index, structurally, and nothing publishes that document yet — so on every current engine the answer isnull. It is capability-dependent like any other read, it computes nothing, and it defines no schema: there is no goal, target, difference or follow-through contract in this SDK, and this method does not promise one is coming or imply what one would look like. Treat a non-null answer as an opaque document from that engine, and read the protocol document before depending on any field inside it.
const history = await bevia.world.history(); // every publication, newest first
const march = bevia.world.at(history.publications[7].manifest);
await march.publication(); // the World as it was published then
await march.document("attention/insights");
const bundle = await bevia.world.publication(); // one document set — take it awayReads serve the published World, never a live replay. No
publication yet reads { ok: false, code: "not_published" }.
world.diff() serves one window: into a publication (the newest, or
{ manifest }), from the empty World or from the publication before it.
Any other range throws a BeviaError with code
published_history_unavailable. attention() and objectives() are
conveniences that return null both when nothing is published and when
the document is empty; read world.document("attention/insights") for the
publication stamp and to tell the two apart.
Ask
const answer = await bevia.ask("what changed in my thinking about pricing this month?");
if (answer.degraded) { /* no AI fuel configured: a deterministic grounded readout */ }When the answer's posture is "clarify", it carries one question with
options. Send the choice back with the same question and the Ask
continues:
if (answer.posture === "clarify" && answer.clarification) {
const c = answer.clarification;
const next = await bevia.ask(question, {
clarification_response: { answers: c.id, chosen: { kind: "referent", label: c.options[0] }, attempts_used: 1 },
});
}ask spends the owner's fuel and is its own capability.
Reality check (Verify)
One engine, two product modes, one MCP door. A Bevia engine serves
mcp.* with its whole tool family. A Verify engine serves the same
door with exactly one tool, reality_check, and realityCheck() wraps
it, typed:
const r = await bevia.realityCheck({ question: "did the last agent finish this?", project: "my-repo" });
if ("code" in r && r.code === "not_published") {
// the answer before the first publication that carries the evidence: say so, ask again later
} else {
console.log(r.answer.word, r.answer.line); // the plain answer, composed once by Verify
for (const c of r.chains) console.log(c.subject, c.assessment, c.rungs.map((g) => `${g.rung}:${g.coverage}`));
console.log(r.publication.basis_seq); // every result names the publication it read
}Scope it from what you know, never from guesses: project, since /
until, revision, path_prefixes, and session (the asking session's
id, to check that session's latest request). Each rung — DECIDED,
CLAIMED, LOCAL, COMMITTED, PUSHED, MERGED, CHECKED_AT_REVISION, CURRENT —
is observed, unsupported or unobservable; a rung Verify cannot see never
counts as a negative, and none of them means "correct". Print answer as
given; no surface composes its own. Nothing is written, no model is
called, nothing is read live: the result is from Verify's published
evidence, and says when it is stale.
On a Verify engine every door this README describes other than pairing,
intake.submit, health and mcp.* answers wrong_mode (a 404 that
names the mode). On a Bevia engine realityCheck() answers tool_error:
that engine does not serve the tool. Declaring a Work — the bounded piece
of work Verify reads against — is the owner's act in their app, not a
door in this package; what a program knows about the work arrives as
evidence through intake.submit.
Events
const sub = bevia.events.on("attention.created", async (ev) => {
const insight = ev.data; // headline, why_now, matter_ids — ids, not evidence text
const detail = await bevia.world.inspect(String(insight.matter_ids?.[0])); // why_now on the event is a typed basis token
// …do whatever you do
});
for await (const ev of bevia.events.subscribe({ kinds: ["world.published"] })) {
console.log(ev.kind, ev.data.version);
}Kinds: world.published, attention.created, capture.admitted,
proposal.created, proposal.resolved, proposal.withdrawn,
proposal.receipt. Every event's id is a cursor — pass it as after
to resume with nothing replayed. No cursor means now; read the World
for the state rather than replaying how it was built.
Propose, get authorized, report
const p = await bevia.propose({
kind: "action",
subject: { text: "the AAPL position", matter_ids: [thesisId] },
proposal: { text: "Reduce the position by 20%." },
basis: { evidence_refs: [insightId], reason: "Two contradicting theses in this week's publication." },
});
const decided = await bevia.proposals.awaitResolution(p.proposal_id); // the OWNER decides, in the app
if (decided.status === "accepted") {
const order = await broker.sell(0.2); // you act; Bevia never does
await bevia.proposals.report(p.proposal_id, {
outcome: "done",
summary: `Sold 20% at ${order.price}, order ${order.id}.`,
source_url: order.url,
});
}A proposal establishes nothing. An accepted action is an
authorization; the receipt lands on the ledger and as one evidence turn
on the proposal's own thread, so the World sees what happened. Bevia
owns was it proposed, who approved it, what authority existed, what
happened. The action belongs to you.
Capabilities
| Capability | Opens |
|---|---|
| capture | intake.* |
| read | world.*, attention, followthrough, objectives, mcp.*, realityCheck — and mcp.* includes ONE write tool, bevia_capture, which the engine admits on that door under read today |
| source_contract | contracts.propose, contracts.list/get — propose the declared structure of a custom source; the owner activates it from the app |
| evidence | raw moment text behind a territory (a door this SDK does not call) |
| ask | ask |
| subscribe | events.* |
| propose | propose, proposals.* |
| substrate_export | exportSubstrate — the WHOLE substrate, owner-grant only. Not to be confused with a Block Share's export, which is a bounded right over one Block and never reaches this door. |
A pairing from the owner's machine gets what it asks for. A pairing from
another device gets its kind's default and the rest is recorded as
pending for the owner to grant in the app. substrate_export and
control are never granted by asking. A BeviaError with status: 401
carries the capability the door needs.
Share-bounded reads
A credential the owner minted against a Block Share is a bounded
reader, whatever capabilities it lists. Its contract is
Share → Block → Rights → Edition: every read it makes must be answered
only from that authorized projection, and a read the engine cannot bound
fails closed. Generic read is never sufficient for a Share credential,
and a Share credential never reaches the owner's whole World. Today the
aperture is served through mcp.* — the tool list narrows to what the
Share's rights admit, every answer is formed from and cites only what
lies inside the Block's edition, and an aperture the engine cannot
evaluate raises BeviaError with code block_aperture_closed. This SDK
carries no Block or Share door, so nothing in it can widen a Share; the
full-World doors (world.*) are the owner's and their paired sensors'.
Errors
Every failure is a BeviaError with status, code, body and, on a
401, capability. code is the engine's own code when it gives one. Three
to know: wrong_mode (this engine's product mode doesn't serve the door;
an engine running Verify serves few of them), published_history_unavailable
(a world.diff() range outside the window above), and stream_error (the
event stream ended with an error; with reconnectMs set, the SDK
reconnects instead). The engine rejects what it cannot understand and says
exactly why; it never forbids what you are building.
The contract underneath
The Bevia Local extension protocol, v1.3 — PROTOCOL.md in every
developer workspace, served as text at bevia.co/build/PROTOCOL.md, and
documented at bevia.co/build. The SDK adds no door the protocol
does not have; if a language has no SDK, that document is the SDK. A
stdlib-only Python reference client of the same doors is among the SDK's
examples in its source; it isn't part of this npm package.
