@tarski/client
v0.9.0
Published
Typed TypeScript client for Tarski backends, generated from the Rust-owned V1 client SDK contract.
Readme
@tarski/client
Typed TypeScript client for Tarski backends: append observations, run
declared and ad-hoc worldview queries, subscribe to event streams, and drive
typed agent sessions against local tarski serve or Tarski Cloud.
Every request, response, receipt, diagnostic, and error type in this package is generated from the same Rust-owned contract the runtime enforces — nothing is hand-maintained, so the types you compile against are the types the backend actually serves. The package has zero runtime dependencies.
The package name is a preview; the embedded contract identity, not the name, is the compatibility anchor.
Install
npm install @tarski/client
# or
pnpm add @tarski/clientNode 18+ or any modern browser bundler. ESM with bundled type declarations.
Quickstart
createTarskiClient ships the batteries-included runtime: isomorphic fetch
(Node 18+ and browsers), bearer auth, generated-validator enforcement on
every response, typed errors keyed on stable codes, and SSE subscriptions
with resume and explicit rehydrate.
import { createTarskiClient } from "@tarski/client";
const client = createTarskiClient({
baseUrl: "http://127.0.0.1:8080", // local `tarski serve` or your Cloud cell
token: () => sessionToken, // in-memory only; never persisted
});
// Append evidence with idempotency — retries return the original receipt.
const receipt = await client.api.appendObservation({
operation: "append_observation",
operationContract: "tarski-client-sdk-api:v1/append_observation",
params: { lineage_id: "lin_orders" },
body: { kind: "order.placed", payload: { order_id: "o_42" }, source: "storefront" },
idempotency: { idempotency_key: "order-o_42" },
});
// Execute a declared query; every page pins the same snapshot and digests.
const page = await client.api.executeDeclaredQuery({
operation: "execute_declared_query",
operationContract: "tarski-client-sdk-api:v1/execute_declared_query",
params: { query_name: "open_orders" },
body: { bindings: {} },
});
// Subscribe to events as a typed async iterable with resume and rehydrate.
for await (const item of client.subscribeLineageEvents({ lineage_id: "lin_orders" })) {
if (item.kind === "rehydrate") {
// Resume was impossible: rebuild visible state from the query
// endpoints at item.rehydrate.head, then keep iterating — the
// subscription continues from the new cursor.
continue;
}
console.log(item.event.event_type, item.cursor); // persist item.cursor to resume later
}Every failure is a TarskiApiError carrying the stable code, the typed
diagnostic body, contracted retryability, and retry_after_ms when the
server sent one. Codes a newer runtime mints that this SDK version does not
know pass through unchanged (contracted: false) — nothing is mislabeled or
hidden.
Actor context is bound, never asserted: hosted requests carry no
caller-supplied authority metadata. The explicit localDevActorContext
option maps to the x-tarski-actor-* fixture headers on local serve only
and fails closed against any other authority. Bearer tokens are accepted as
in-memory values or provider functions; the SDK has no persistence API.
The generated client is transport-injected: new ClientSdkApiClient(transport)
accepts any ClientSdkApiTransport when you need custom plumbing, and
createTarskiFetchTransport is the default implementation.
The operation surface covers observation append (single and batch — batch
appends are ordered but contract no idempotency, so a batch retry appends
again), lineage and plane reads (observations, facts, intents, effects),
declared and ad-hoc queries with deterministic cursors, SSE subscriptions
for lineage and session events, the typed session lifecycle
(create-or-start, start, preview, messages, transcript, provenance), and
capability discovery. It also covers workspace schedule list/get/create/
replace/pause/resume/remove/trigger/fires/preview and blob upload/inventory/
metadata. client.downloadBlob() owns raw bytes and HTTP range requests
because a successful blob download is intentionally not JSON.
Capability discovery is explicit and fail-closed. Resolve the generated
operation you intend to call; an authority that omits it yields the typed
unsupported_runtime state and the SDK never probes a guessed legacy route:
import {
clientSdkCapabilityIsAvailable,
resolveClientSdkCapability,
} from "@tarski/client";
const capabilities = await client.api.discoverCapabilities({
operation: "discover_capabilities",
operationContract: "tarski-client-sdk-api:v1/discover_capabilities",
});
if (!clientSdkCapabilityIsAvailable(capabilities, "createSchedule")) {
console.log(resolveClientSdkCapability(capabilities, "createSchedule").state);
}Coherent reads and reactive query updates
Use one query batch when several windows must describe the same committed worldview:
const dashboard = await client.api.executeQueryBatch({
operation: "execute_query_batch",
operationContract: "tarski-client-sdk-api:v1/execute_query_batch",
body: {
lineage: "org:acme",
queries: [
{ name: "open-tickets", bindings: {} },
{ name: "on-call-summary", bindings: {} },
],
},
});
// Every result carries dashboard.snapshot: the same lineage, evaluator, and head.subscribeQuery() delivers an initial full row set, then exact row deltas.
Resume is the opaque per-subscription updateId. A rehydrate item replaces
the caller's whole visible row set; it is never a pretend replay. A closed
item is terminal.
for await (const update of client.subscribeQuery("open-tickets", {
lineage: "org:acme",
})) {
switch (update.kind) {
case "initial":
case "rehydrate":
rows = update.rows;
break;
case "delta":
rows = applyDelta(rows, update.added, update.removed);
persistResumeCursor(update.updateId);
break;
case "closed":
return;
}
}Schedules and blobs
await client.api.createSchedule({
operation: "create_schedule",
operationContract: "tarski-client-sdk-api:v1/create_schedule",
params: { lineage_id: "org:acme" },
body: {
schedule_id: "daily-digest",
definition: { cron: "0 7 * * *", timezone: "Europe/Rome" },
},
});
const uploaded = await client.api.uploadBlob({
operation: "upload_blob",
operationContract: "tarski-client-sdk-api:v1/upload_blob",
body: {
lineage_id: "org:acme",
content: "hello",
media_type: "text/plain",
},
});
const bytes = await client.downloadBlob(String(uploaded.digest));Platform-builder control plane
Point a separate client instance at the management-plane origin and provide a management credential from memory. The generated methods can mint, rotate, list, and revoke one tenant user's delegated runtime token, inspect tenant lineages, and provision an empty lineage with an optional observation-only genesis batch:
const created = await management.api.provisionTenantLineage({
operation: "provision_tenant_lineage",
operationContract: "tarski-client-sdk-api:v1/provision_tenant_lineage",
body: {
organization_id: "org_acme",
tenant_id: "tenant-acme",
app_id: "truepenny",
environment_id: "production",
cell_id: "cell-1",
lineage_id: "tenant-acme:user-42:primary",
idempotency_key: "signup:user-42",
genesis_observations: [{
kind: "account.created",
payload: { user_id: "user-42" },
}],
},
idempotency: { idempotency_key: "signup:user-42" },
});Tenant lineage prefixes are stored on the tenant and enforced before the management plane dispatches to a runtime cell. Genesis accepts observations only: derived facts, intents, effects, provenance, and world documents are rejected as import truth. Delegated-token revocation returns only after the runtime cell acknowledges the revocation record.
Contract identity
Every build embeds the identity of the exact contract it was generated from:
import { CLIENT_SDK_CONTRACT_IDENTITY } from "@tarski/client";
CLIENT_SDK_CONTRACT_IDENTITY.contractVersion; // "tarski-client-sdk-api:v1"
CLIENT_SDK_CONTRACT_IDENTITY.sourceContractDigest; // "sha256:…"
CLIENT_SDK_CONTRACT_IDENTITY.generatorVersion; // "tarski.client_sdk.generator.v1"The published package also embeds the generated contract sources verbatim
under dist/contract/ with a provenance stamp. Versioning is mechanical: a
changed source contract digest cannot ship without at least a minor version
bump, enforced by the release gate against contract-baseline.json.
Errors are typed, keyed on stable codes
Failures carry the stable codes from the V1 registries — live ingress, session lifecycle, worldview query, backpressure, and blob storage — with contracted retryability. Key behavior on codes, never on prose:
import { clientSdkErrorRetryabilityForCode } from "@tarski/client";
clientSdkErrorRetryabilityForCode("idempotency.conflict"); // "non_retryable"
clientSdkErrorRetryabilityForCode("lineage_busy"); // "retryable"
clientSdkErrorRetryabilityForCode("stream.resume_window_exceeded"); // "rehydrate_required"Development (Tarski repository)
Requires Node 22+ and pnpm 10 (pinned via packageManager). This tooling is
for developing the SDK itself, not for building Tarski apps.
pnpm install --frozen-lockfile
pnpm run check # boundary check, typecheck, build, smoke test, release gateThe build copies the generated client and validators from
spec/tarski/v1/client-sdk-api.* into src/generated/ — never edit those
copies or declare contract types by hand. pnpm run lint:boundaries and
pnpm run check:release-gate enforce the generated boundary and the
digest-bound version policy.
License
Apache-2.0 — see the LICENSE file at the repository root.
