HOLONOGRAPH / The Guide · SDK reference v0.26.0 · client 0.3.0 all chapters

Chapter 05

SDK reference

Reflects Holonograph v0.26.0 · @holonograph/client 0.3.0 · ~12 min read

The HolonographClient is the operator-facing surface end-to-end: one class you construct, one call flow (messages.create → handle → reportOutcome), and two auxiliary namespaces (contract, availability) for shape and gap declarations. This chapter documents every method.

Install

npm install @holonograph/client
# or
pnpm add @holonograph/client
# or
yarn add @holonograph/client

Single self-contained ES module, no runtime dependencies. Targets Node 18+ and any runtime with a global fetch. Fully typed; the package ships its own type declarations.

Construct a client

import { HolonographClient } from "@holonograph/client";

const client = new HolonographClient({
  endpoint: "http://127.0.0.1:8080",
  token: process.env.HOLONOGRAPH_TOKEN,
  runMode: "production",
  lensVersion: "lv_2026_07_01",
  substrate: {
    lensVersion: "lv_2026_07_01",
    lightSourceIdentifier: "anthropic/claude/4",
    runMode: "production",
    provenance: "production",
    operatorColumns: {}
  }
});

Constructor options

OptionShapeNotes
endpointstringURL where your running Holonograph lens is reachable. The README ships with http://127.0.0.1:8080 as its example host.
tokenstring (opt)Bearer token, when the lens requires one.
runModeenum (opt)One of the canonical modes (production, test, eval, replay, local_dev). Sent as x-holonograph-run-mode on every request. Can be resolved per-call at request time; see Run modes.
lensVersionstringIdentifier of the lens configuration this client should call under. Pinned into every evaluation event this client produces.
substrateobjectThe SubstrateSnapshot attached to every event. Carries lensVersion, lightSourceIdentifier, runMode, provenance, and operatorColumns; see below.
emitobject (opt)Delivery mode + buffering configuration for evaluation events. Four modes (sync, buffered, otlp, dual) trade caller latency against throughput and export path. Defaults to { mode: "sync" }. See Delivery modes below.

The substrate object

FieldShapeNotes
lensVersionstringWhich lens configuration was in effect.
lightSourceIdentifierstringWhich model produced the output, as vendor/model/version.
runModeenumWhich run mode the evaluation ran under.
provenancestringOrigin category of the run; see Substrate columns.
operatorColumnsobjectThe operator-declared deployment-state columns, keyed by column id. For example: { prompt_hash: "v2-sha256-…", tool_schema_version: "3.1.0" }.

Delivery modes: the emit option

The client defaults to synchronous delivery: every reportOutcome awaits the POST /lens/events round-trip and propagates errors to the call site. That is the safe default, and for many operators it is enough. When the evaluation call path becomes hot enough that caller latency starts to matter, or when events need to leave the process on an OpenTelemetry export path, three additional modes are available.

ModeWhat it does
syncDefault. reportOutcome awaits the HTTP POST; errors throw at the call site.
bufferedBounded ring buffer, drop-on-overflow. reportOutcome resolves on enqueue; a background drain flushes to /lens/events. Fire-and-forget safe.
otlpBuffered, but the drain fans out as OpenTelemetry spans to an OTLP/HTTP traces endpoint (the sidecar or collector of your choice).
dualBuffered, and each drained batch fans out to both /lens/events and OTLP/HTTP, with per-channel delivery tracking. The spans on the OTLP side carry only the event index; the bulk payload lives in the HTTP body, and the collector reconciles by eventId.

All three buffered modes share the same bounded buffer and the same completeness accounting. What differs is the drain channel.

Configuration

const client = new HolonographClient({
  endpoint: "http://127.0.0.1:8080",
  runMode: "production",
  lensVersion: "lv_2026_07_01",
  substrate: { /* ... */ },
  emit: {
    mode: "dual",
    bufferSize: 2048,
    flushIntervalMs: 1000,
    sentinelIntervalMs: 30000,
    windowMs: 10000,
    otlp: {
      endpoint: "http://127.0.0.1:4318/v1/traces",
      headers: { authorization: "Bearer …" },
      maxQueueSize: 4096
    },
    onWindow: (w) => metrics.record(w),
    onFlushError: (err, event) => log.warn("flush failed", { err, event }),
    onExtremeSustainedLoad: (e) => alerts.page("holonograph drop rate elevated", e)
  }
});
FieldShapeNotes
modeenumOne of sync, buffered, otlp, dual. Default sync.
bufferSizenumber (opt)Max events held in the ring buffer at once. Default 2048. Once full, the oldest events are dropped and counted.
flushIntervalMsnumber (opt)How often the buffer is drained to the wire. Default 1000ms.
sentinelIntervalMsnumber (opt)How often a session-liveness sentinel span is emitted on span-carrying channels. Default 30000ms.
windowMsnumber (opt)Length of one completeness-accounting window. Default 10000ms.
otlpobject (opt)OTLP/HTTP traces endpoint configuration. Required for otlp and dual modes; see below.
onWindowfunction (opt)Called once per completed accounting window with the window's CompletenessWindow snapshot.
onFlushErrorfunction (opt)Called on per-event flush failures under buffered modes. Default console.warn. Under sync mode, flush errors throw at the call site instead.
extremeSustainedLoadobject (opt){ dropRateThreshold?: number, sustainedForMs?: number }. When drops exceed the rate for the sustained duration, onExtremeSustainedLoad fires. Default 1% for 60000ms.
onExtremeSustainedLoadfunction (opt)Called when the extreme-load condition is met. Route to your alerting.

emit.otlp

FieldShapeNotes
endpointstringOTLP/HTTP traces URL. For example, http://127.0.0.1:4318/v1/traces.
headersobject (opt)Extra request headers on the OTLP export (auth tokens, tenant hints, whatever your collector expects).
maxQueueSizenumber (opt)OTLP processor queue size. Defaults to 2 × bufferSize.

Reading delivery completeness

A bounded buffer with drop-on-overflow only produces trustworthy analysis if the drops are counted. The client publishes an accounting window every emit.windowMs and exposes the current window snapshot as a read.

const window = client.completenessWindow();
if (window?.biasFlag) {
  // drops occurred in this window; downstream analyses should treat
  // the sample as potentially non-random over this interval
}

client.completenessWindow(): CompletenessWindow | null

Returns the current window's snapshot. Returns null under sync mode (no buffer, nothing to account for).

FieldShapeNotes
lensVersionstringWhich lens version this window is accounted under.
windowStartMs, windowEndMsnumberWindow boundaries, in wall-clock ms.
offerednumberEvents received from the caller during the window.
droppednumberEvents dropped at the buffer boundary (ingress-time bound).
deliverednumberEvents successfully egressed across all drain channels.
failednumberEvents that reached a drain channel but the wire rejected.
channelsobjectPer-channel breakdown (http, otlp), each with { attempted, delivered, failed }. Distinguishes /lens/events loss from OTLP-export loss.
completenessLowerBoundnumberThe ratio (offered − dropped) / offered. A window's floor coverage.
biasFlagbooleantrue when drops occurred in the window. Signals the sample can no longer be treated as random over this interval.

client.shutdown(): Promise<void>

Flushes any remaining buffered events and stops the background drain. Call before process exit under buffered modes so an in-flight window doesn't die with the process. No-op under sync mode.

process.on("SIGTERM", async () => {
  await client.shutdown();
  process.exit(0);
});
The guarantee Buffered modes are at-most-once. There is no reordering, no retry, no hidden growth. Drop-on-overflow is the only bound; the drops are counted and surfaced as biasFlag. Fire-and-forget stays fire-and-forget.

The call flow

One evaluation is three calls: messages.create, then use .result, then .reportOutcome(...).

const handle = await client.messages.create({
  surfaceId: "classify-intent",
  messages: [{ role: "user", content: "why was I charged twice?" }],
  tools: [ /* ... */ ]
});

const answer = handle.result;             // read/act on the model's response
const isCorrect = grade(answer);          // your evaluation logic

await handle.reportOutcome({
  dimensions: [{
    dimensionId: "correctness",
    passed: isCorrect,
    expected: "a correct answer to the user's question",
    actual: summarize(answer)
  }],
  cohortTags: ["customer-tier=enterprise"]
});

messages.create() mediates the model call and returns a handle. handle.result is the response you use in your agent. handle.reportOutcome() closes the loop; it persists the evaluation event that binds this call into your history.

messages.create(req)

Every field on the request is optional except surfaceId and messages. The rest are passed through to the model when relevant, or interpreted by the lens.

FieldShapeNotes
surfaceIdstring (req)The surface this call belongs to. Must exist on the active contract.
messages[]array (req)The conversation turns.
systemstring (opt)System prompt.
systemOverridestring (opt)Overrides the system prompt for this call only.
additionalSkillsstring[] (opt)Extra skills to inject for this call.
additionalContextTagsstring[] (opt)Extra context tags this call should carry.
lightSourceIdOverridestring (opt)Overrides the surface's default routing to a specific vendor.
maxOutputTokensnumber (opt)Cap on the model's output length.
temperaturenumber (opt)Model sampling temperature.
toolsarray (opt)Tool declarations for the model to invoke.
toolChoiceobject (opt)Which tool the model must invoke, if any.
cacheControlobject (opt)Caching hints for the model provider.
correlationIdstring (opt)Pass one in to bind this call to a prior call's correlation. Otherwise generated.

The call handle

The object returned by messages.create().

MemberShapeNotes
.resultobjectThe primary model's response. Use this as the answer in your agent.
.observerRecords[]arrayUnder multiplex routing, the observer vendors' captured records. Empty when not multiplexing.
.reportOutcome(outcome)methodPersist the evaluation event that closes this call.
.gradeObserver(recordOrId, outcome)methodAttach grades to a cross-vendor observer call before reporting, when the lens returned observer records. Call this before reportOutcome.

reportOutcome(outcome)

FieldShapeNotes
dimensions[]array (req)One entry per dimension declared on the surface. Each entry: { dimensionId, passed, expected, actual }.
cohortTags[]string[] (opt)Discrete labels to slice on later. For diagnostic substrate columns, use the <column.id>=<value> form.
sideEffects[]array (opt)Any downstream actions this call triggered; useful for correlated fixtures and lessons later.
fixtureIdstring (opt)The fixture this call satisfied, when the call was run as part of a conformance check.

Dimension entry shape

FieldShapeNotes
dimensionIdstringMust match a dimension declared on the surface's contract.
passedbooleanWhether the dimension passed the evaluation.
expectedstringWhat good looked like: the expected shape of the response.
actualstringWhat the model actually produced (or a short summary).

Calling reportOutcome persists the evaluation event to Holonograph's substrate: one row, four layers bound (see The four-layer snapshot). What Holonograph does with that event afterwards is the subject of the analysis chapters.

Multiplex: grade the observers

When the surface's contract routes to more than one vendor, the primary's response comes back as handle.result and the observers' captured records come back as handle.observerRecords[]. Grade the observers before reportOutcome so all vendors' outcomes land in the same evaluation.

for (const rec of handle.observerRecords) {
  await handle.gradeObserver(rec, {
    dimensions: [{
      dimensionId: "correctness",
      passed: isCorrect(rec.result),
      expected: "a correct answer to the user's question",
      actual: summarize(rec.result)
    }]
  });
}
await handle.reportOutcome({
  dimensions: [{
    dimensionId: "correctness",
    passed: isCorrect(handle.result),
    expected: "a correct answer to the user's question",
    actual: summarize(handle.result)
  }]
});

See The multiplexer for how routings are declared.

Contract

contract.register(contract)

Registers or replaces the surface contract for this agent. Returns a ContractSummary reflecting the newly-active shape. See Surface contracts for the full contract shape and the summary fields.

const summary = await client.contract.register({
  agentId: "billing-triage",
  surfaces: [ /* ... */ ]
});
console.log(summary.surfaces[0].dimensionCount);

Availability

availability.mark(req, opts?)

Records that a surface is (or is not) available for evaluation over a given interval. Two modes:

  • Single: one transition event: "this surface just became unavailable" or "…just came back." Pass mode: "single" with a transition.
  • Paired: bound an interval you already know the extent of. Pass mode: "paired" with unreachableFrom and unreachableUntil. The response includes a pairing reference so downstream analyses can bracket the interval cleanly.
await client.availability.mark({
  mode: "single",
  surfaceId: "classify-intent",
  transition: "unavailable",
  reason: "vendor-outage"
});

Response:

{ "eventIds": ["evt_..."], "pairing": null }

Headers & run mode resolution

The client sends two headers on every request:

  • Authorization: Bearer <token>: when the client was constructed with a token.
  • x-holonograph-run-mode: <mode>: from the client's runMode, unless resolved per-call. A missing mode is rejected by the daemon by design (fail-closed).

For scenarios where the run mode is determined at request time (Express middleware, async work, custom resolution), see Run modes.

Escape hatch: callDirectly

For scenarios that need the full request shape (a bespoke bridge, a replay tool, or an integration test that wants to prod an edge of the wire), the client exposes a lower-level callDirectly method that skips the messages.create ergonomics and forwards the request as-authored.

const result = await client.callDirectly(rawRequest);

Prefer messages.create for anything user-facing. callDirectly is an escape hatch, not the everyday path.

Errors

Errors from the lens surface as HolonographHttpError, which carries the HTTP status, an error code, and any details the lens returned. There are additional typed error classes for the grading and availability flows so a catch block can discriminate on class.

import { HolonographClient, HolonographHttpError } from "@holonograph/client";

try {
  await handle.reportOutcome(outcome);
} catch (err) {
  if (err instanceof HolonographHttpError) {
    console.error(err.status, err.code, err.details);
  } else {
    throw err;
  }
}

The underlying wire shape every error carries:

{
  "error": "human message",
  "code": "error_code",
  "details": { }
}

4xx covers validation, auth, and not-found. 5xx is internal. 501 means the feature isn't wired on the given deployment.

Advanced: HttpTransport

The client is built on an HttpTransport primitive that's exported for advanced use: swapping in a custom fetch, adding request interceptors, or running the transport under a different runtime harness. Most integrations never touch it. If yours does, the transport surface is fully typed; the tests in the package are the working reference.