@holonograph/client
v0.14.0
Published
TypeScript HTTP client for a Holonograph lens. Send messages through the lens and report scored outcomes back to it.
Downloads
1,440
Maintainers
Readme
@holonograph/client
The TypeScript HTTP client for a Holonograph lens.
A Holonograph lens sits in front of your LLM calls and turns every call into a longitudinal, gradeable record. This package is the client you integrate into your application: it sends messages through the lens over HTTP and reports the scored outcome of each call back to it. Nothing else — no engine, no model keys, no server. The lens does the work; this is the wire to reach it.
Install
npm install @holonograph/clientThe package is a single self-contained ES module with no runtime
dependencies. It targets Node 18+ and any runtime with a global fetch.
Quickstart
import { HolonographClient } from '@holonograph/client';
const client = new HolonographClient({
// Where your running lens is reachable (the binary's local default port).
endpoint: 'http://127.0.0.1:8787',
// Optional bearer token, if the lens requires one.
token: process.env.HOLONOGRAPH_TOKEN,
// Optional run mode, forwarded on every request.
runMode: 'production',
// Pinned into every evaluation event this client produces.
lensVersion: 'lv_2026_07_01',
substrate: {
lensVersion: 'lv_2026_07_01',
lightSourceIdentifier: 'anthropic/claude/4',
runMode: 'production',
provenance: 'production',
operatorColumns: {},
},
});
// 1. Send a message through the lens.
const handle = await client.messages.create({
surfaceId: 'support.triage',
messages: [{ role: 'user', content: 'My order never arrived.' }],
});
// Read the model's output off the returned handle.
const result = handle.result;
// 2. Score the call however you like, then report the outcome back to the lens.
await handle.reportOutcome({
dimensions: [
{
dimensionId: 'is_actionable',
passed: true,
expected: 'a concrete next step',
actual: 'offered to open a replacement order',
},
],
});That is the whole loop: messages.create() → score → reportOutcome(). The
lens assembles and stores the evaluation event; over time those events become
the longitudinal record you query and visualize.
Verdict reliability — let the lens check your judge
A score is only as trustworthy as the judge that produced it, and an LLM judge can
misread its own input: accuse the model of inventing a value that was right there in
a tool result, or of skipping a tool it actually called. The lens catches that
deterministically — but only if your judge hands it a structured claim to check,
instead of burying the accusation in prose. Attach a claim to any judged dimension:
await handle.reportOutcome({
dimensions: [
{
dimensionId: 'grounded',
passed: false,
expected: 'only facts present in the tool results',
actual: 'claimed a refund was issued and cited a total of $84.20',
// What the judge ASSERTED, as fields the lens can verify — not prose.
claim: {
status: 'emitted',
assertions: [
// "the model cited a value that is not in what it read"
{ kind: 'datum-absent', datum: '$84.20', reference: 'the lookup total was $48.20' },
// "the model claimed an action without calling the tool that performs it"
{ kind: 'tool-not-invoked', tool: 'issue_refund' },
],
},
},
],
});The lens checks each assertion against the captured call — is the accused value actually absent from everything the model read? was that tool really never invoked, anywhere across the whole conversation? — and reports, per assertion, whether the record corroborates or contradicts the judge. A contradiction is the loud case: the judge's verdict rested on something it misread, so that score can't be trusted. Two assertion kinds ship today:
datum-absent— a value the judge says was fabricated. It splits the accuseddatumfrom the optionalreferencethe judge measured it against, so the lens verifies the accusation, never the yardstick.tool-not-invoked— a tool the judge says was never called. The check reads the whole captured conversation, so a call made in an earlier turn still counts.
A dimension with no claim is simply not claim-instrumented — the lens stays
honest-null about it rather than guessing, so you opt in one dimension at a time. If
your judge is meant to emit a claim but its output is malformed, report
{ status: 'malformed', reflectionAttempted: <boolean> } — the lens raises that as a
judge-malfunction signal rather than silently trusting a broken instrument. The
JudgeClaimReport and JudgeAssertion types are exported so you can build and
validate the claim before you send it.
Read the whole layer back over a window from your lens — every claim-instrumented dimension with its per-assertion disposition:
GET /holonograph/verdict-reliability(There is no dedicated client method for this read yet; use client.callDirectly or a
plain fetch against your lens endpoint.)
What it speaks
The client talks to a lens over plain HTTP under the /holonograph/* path
space (plus the lens's event and availability endpoints). Requests carry
x-holonograph-* headers for correlation and run mode. You never construct
these by hand — the client does.
Errors from the lens surface as HolonographHttpError, which carries the HTTP
status, an error code, and any details the lens returned.
API surface
HolonographClient— construct with anendpoint(plus optionaltoken/runMode) and thelensVersion+substrateto pin into every event.client.messages.create(request)— send a message; returns a handle.handle.reportOutcome(outcome)— commit the scored outcome. Each dimension may carry aclaim(aJudgeClaimReport) for the verdict-reliability layer to check (see above). TheJudgeClaimReport/JudgeAssertion/JudgeAssertionKindtypes are exported.handle.gradeObserver(...)— attach grades to a cross-vendor observer call before reporting, when the lens returned observer records.client.contract.register(contract)— publish a surface contract to the lens.client.availability.mark(request)— write availability markers.client.callDirectly(request)— escape hatch for the full request shape.
HttpTransport— the underlying transport, exposed for advanced use.HolonographHttpErrorand the typed error classes for the grading and availability flows.
Everything is fully typed; the package ships its own type declarations.
Watching for changes
Pull-side alerting, transport-agnostic. Poll any windowed lens read on a cadence and act only on the delta — a steady state is silent (the same "no all-quiet pings" discipline the lens applies to its own output). You supply the fetch and where the change goes; the client supplies the cadence and change-detection.
watchByKey({ poll, intervalMs, keyOf, onChange })— poll a collection and fireonChange(diff)only when items areadded/removed/changed(matched bykeyOf). Returns a handle with.stop(). The first poll silently seeds the baseline, so you only hear about change since the watch started (passemitInitial: truefor a cold-start inventory).diffByKey(prev, next, keyOf)/hasChanges(diff)— the underlying keyed diff, if you want change-detection without the loop.createPoller({ poll, intervalMs, onResult })— the bare cadence loop (non-overlapping; errors isolated toonError, never kill the watcher).watchReconciliation(...)/watchEmission(...)— typed sugar overwatchByKeyfor the lens's reconciliation + emission reads.
import { watchByKey } from '@holonograph/client';
const handle = watchByKey({
poll: (signal) => fetchReconciliation({ signal }), // your windowed read
intervalMs: 30_000,
keyOf: (row) => row.emissionStreamId,
onChange: (diff) => notifyOnCall(diff), // added / removed / changed only
});
// later: handle.stop();Requirements
This client does nothing on its own — it needs a running Holonograph lens to
connect to. Point endpoint at your lens and you are set.
License
MIT
