Skip to content

17. TypeScript SDK — `@eigenius/client`

The TypeScript SDK at clients/eigenius-ts/ wraps the orchestrator’s two Connect-RPC services in a single typed Eigen class. It is the same SDK the notebook uses (chapter 14); you can use it from any TypeScript runtime that speaks Connect-RPC — Deno scripts, browser apps, Node.js servers — to drive the kernel programmatically.

Per D22 §5.

17.1. What it covers

The Eigen class has 33 public methods — 30 async plus three synchronous accessors (getEndpoint, getDefaultBranch, useBranch). Async responses are protobuf message objects (camelCased fields).

The table below covers the eleven the notebook MVP drives. The rest are grouped after it; the authoritative list is the class itself.

MethodPurposeUnderlying RPC
eigen.health()Liveness checkEigeniusKernel.Health
eigen.inspect(iri, options?)Resolve a resource by IRI from the active layerEigeniusKernel.Inspect
eigen.query(eigenql, options?)Run an EigenQL query, get an Eigon-CBOR ResultSet documentEigeniusKernel.Query
eigen.load(source, options?)Compile + commit a layer (ESL source or Eigon-JSON document)EigeniusKernel.Load
eigen.validateProgram(program, options?)Run a program’s static checks against the active layer chain; checksPerformed names which ran (no EigenTT type-check — issue #143)EigeniusKernel.ValidateProgram
eigen.runProgram(program, input, options?)Run a program inline against an inline inputEigeniusKernel.RunProgram
eigen.runProgramByIri(programIri, inputIri, options?)Run a program already loaded into the chain by IRI against an input also by IRI — the natural “one program × N inputs” shapeEigeniusKernel.RunProgramByIri
eigen.listInstitutions()List registered institutions and their declared fiber structureEigeniusKernel.ListInstitutions
eigen.layerTopology(options?)Walk the layer chain; nodes for layers + classes + properties + institutions, edges for parent / is_a / subclass / requires / property-refNotebookService.LayerTopology
eigen.publishNotebook(notebook)Translate a NotebookJson into Notebook + Cell resources (content-addressed IRIs), then load themcalls Eigen.load internally

The remaining twenty-two, by area:

AreaMethods
Client state (sync)getEndpoint, getDefaultBranch, useBranch
Tracesreflect
BrancheslistBranches, getBranch, createBranch, deleteBranch, mergeBranches
Merge resolutionpreviewMerge, prepareMerge, previewCascade, submitResolution
ConsolidationestimateConsolidation, consolidateChain
TaskslistTasks, getTaskStatus, cancelTask
TagscreateTag, listTags, deleteTag
Garbage collectionestimateGc, runGc

prepareMerge is worth singling out: it is the only surface in the whole platform that returns merge conflict ids, and the CLI has no equivalent subcommand. A caller resolving a merge from the CLI has to obtain the ids through this method, the raw PrepareMerge RPC, or the notebook.

The SDK also exports the underlying request/response types (LoadResponse, QueryResponse, RunProgramResponse, LayerTopologyResponse, TopologyNode, TopologyEdge, NodeKind, EdgeKind, etc.) and notebook-specific types (NotebookJson, CellJson, notebookJsonToResources, resourcesToNotebookJson).

Source: clients/eigenius-ts/src/client.ts (the Eigen class), clients/eigenius-ts/src/notebook.ts (notebook-publish translator), clients/eigenius-ts/mod.ts (public exports).

17.2. Construction

import { Eigen } from "@eigenius/client";
// Browser / single-origin: hits the same origin, expects the
// orchestrator's static-file route to be live.
const eigen = new Eigen({ endpoint: window.location.origin });
// Deno / Node / explicit endpoint:
const eigen = new Eigen({ endpoint: "http://localhost:8080" });

EigenOptions:

FieldRequiredDescription
endpointyesOrchestrator base URL — e.g. http://localhost:8080
fetchnoOverride the global fetch (Deno tests, custom interceptors)
bearerTokennoCurrently unused; the hook for token auth without an API change later (D22 §8.3)
defaultBranchnoBranch used when a call doesn’t pass an explicit branch. Empty / omitted = the server’s default (main). See §17.4 for branch semantics.

17.3. Five-line examples

Liveness + inspect

const h = await eigen.health();
console.log(`kernel v${h.version}, ${h.layerCount} layer(s), ${h.resourceCount} resource(s)`);
const cls = await eigen.inspect("urn:eigenius:core:Class");
console.log(cls.found ? `Class CBOR is ${cls.resource.length} bytes` : "not found");

Load ESL → query

const { success, layerId, resourceCount, errors } = await eigen.load(`
namespace ex = "urn:eigenius:demo:ex";
class ex:Widget {
description = "A widget.";
requires ex:label;
}
property ex:label : core:string { description = "Label."; }
`);
if (!success) throw new Error(errors.map((e) => e.message).join("; "));
const q = await eigen.query(`
USING "urn:eigenius:core:Class"
MATCH Class(?c) { short_name: ?n }
RETURN [] { name: ?n }
ORDER BY ?n
`);

q.document is an Eigon-CBOR document containing the ResultSet + row class + row Property metadata + row resources. Decode it with cbor-x (the same package the notebook uses) — see notebooks/src/runtime/resultDocument.ts for a worked decoder.

Run a program by IRI

// Assumes a previous eigen.load(...) committed both the program
// (analyze_patent) and the input (US10452978B2) to the active chain.
const result = await eigen.runProgramByIri(
"urn:eigenius:demo:patent:analyze_patent",
"urn:eigenius:demo:patent:US10452978B2",
);
console.log(`success=${result.success}, trace=${result.traceIri}`);
// result.output is a CBOR-encoded PatentBrief resource;
// result.traceIri points at the ProgramTrace if a trace store is configured.

The runProgram (inline) variant takes both program and input as bytes with a single content_type — useful when neither is in the chain yet.

Walk the layer chain

import { NodeKind } from "@eigenius/client";
const topo = await eigen.layerTopology({ includeResources: false });
const layers = topo.nodes.filter((n) => n.kind === NodeKind.LAYER);
const classes = topo.nodes.filter((n) => n.kind === NodeKind.CLASS);
console.log(`${layers.length} layers, ${classes.length} classes`);

Set includeResources: true to also get one node per Resource instance (heavier; handy for debugging / full-graph rendering).

Publish a notebook

import type { NotebookJson } from "@eigenius/client";
const notebook: NotebookJson = JSON.parse(
await Deno.readTextFile("./my-notebook.json"),
);
const { publish, load } = await eigen.publishNotebook(notebook);
console.log(`notebook IRI: ${publish.notebookIri}`);
console.log(`${publish.cellIris.length} cell IRI(s) in the published layer ${load.layerId}`);

Identical content yields the same notebookIri (content-addressed). See chapter 14 §14.5.

17.4. Branches

Phase 14g made every load / runProgram / runProgramByIri / reflect call branch-aware. The TypeScript client mirrors the gRPC surface: per-call branch overrides on the relevant methods, plus a defaultBranch constructor option and a useBranch(name) mutator for setting the client-wide default after construction. Empty / omitted on a call falls back to the client default; empty / omitted on the constructor falls back to the server default (main).

// Default to a feature branch for the lifetime of this client
const eigen = new Eigen({
endpoint: "http://localhost:8080",
defaultBranch: "feature-x",
});
// All commits land on feature-x
await eigen.load(eslSource, { contentType: "application/x-esl" });
// Override for one call — commit to main even though the default is feature-x
await eigen.load(eslSource, {
contentType: "application/x-esl",
branch: "main",
});
// Mutate the default after construction
eigen.useBranch("experiment-42");
console.log(eigen.getDefaultBranch()); // "experiment-42"

For reads, branch and atLayer are mutually exclusive — pass either to pin the read, leave both empty to use the (default) branch’s current head:

// Pin to a specific layer
const r1 = await eigen.inspect("urn:example:Dog", { atLayer: "abe85ea7..." });
// Pin to a branch's current head
const r2 = await eigen.inspect("urn:example:Dog", { branch: "feature-x" });

Branch CRUD methods (require a kernel with a persistent backend):

// List
const branches = await eigen.listBranches();
for (const b of branches) console.log(`${b.name}: ${b.headLayer}`);
// Show
const main = await eigen.getBranch("main");
if (main.found) console.log(`main is at ${main.headLayer}`);
// Create — branch off main's head
const created = await eigen.createBranch("feature-x", { fromLayer: main.headLayer });
if (!created.success) console.error(created.error);
// Delete (defaults to safe — refuses if a task pin matches the head)
const del = await eigen.deleteBranch("feature-x");
console.log(del.deleted ? `deleted (was at ${del.previousHead})` : "did not exist");
// Force-delete (skip the task-pin safety check)
await eigen.deleteBranch("feature-x", { force: true });

See chapter 4 §4.6 for the equivalent CLI surface and D23 §5.4–§5.5 for the design.

17.6. Error handling

Most methods return structured response objects with success: bool and an errors: ValidationError[] array — non-RPC failures (validation, parse, etc.) come back this way and the call doesn’t throw.

Connect-RPC errors (network failures, kernel-side gRPC Status errors that the orchestrator translates) throw ConnectError (from @connectrpc/connect). The orchestrator’s passthrough explicitly re-wraps kernel gRPC errors so the inbound Connect protocol carries the actual message — without this, browser callers see [internal] HTTP 400 with the real message URL-encoded into a grpc-message header. See orchestration/src/notebook/eigenius_kernel_passthrough.ts.

17.7. Layout and dependencies

clients/eigenius-ts/
├── deno.jsonc # Deno project config (npm: + jsr: imports)
├── package.json # npm package shape (name, exports, deps)
├── mod.ts # public API exports
├── src/
│ ├── client.ts # Eigen class
│ └── notebook.ts # NotebookJson <-> Resource[] translator + content-addressed IRIs
├── generated/ # buf-generated Connect stubs (do not edit)
│ └── eigenius_pb.ts # generated by `npx buf generate`
└── examples/
├── smoke-test.ts # 8 steps; covers the core RPCs, not every method
├── publish-test.ts # publishNotebook end-to-end
└── error-test.ts # demonstrates the orchestrator's gRPC→Connect error translation

Runtime deps: @bufbuild/protobuf@^2, @connectrpc/connect@^2, @connectrpc/connect-web@^2. The notebook consumes the SDK as a file:../clients/eigenius-ts workspace dep; once the SDK stabilises, npm publication via dnt (Deno-to-Node) will follow.

17.8. Regenerating the proto stubs

The buf pipeline lives at the repo root (buf.yaml + buf.gen.yaml). The SDK’s generated/ is one of two output targets (the other being the orchestrator’s src/gen/). Regenerate after any change to proto/eigenius.proto:

Terminal window
npx --yes @bufbuild/buf generate

Buf is pinned at v2; both targets emit protoc-gen-es target=ts, which produces idiomatic Connect-Web TypeScript stubs.

17.9. Smoke test

The smoke test at clients/eigenius-ts/examples/smoke-test.ts exercises the core RPCs against a live stack:

Terminal window
cd clients/eigenius-ts
deno run --allow-net --allow-env examples/smoke-test.ts
# expects http://localhost:8080 by default; override with EIGENIUS_ORCHESTRATOR=...

Output is an 8-step transcript (TOTAL_STEPS = 8): health → inspect → query → listInstitutions → layerTopology (taxonomy + full) → load → validateProgram → branch CRUD + per-call routing. This is the SDK’s Phase 1 acceptance criterion (D22 §5.7).

17.10. Design references


Next: 18. Appendix →