@littlebigbrain/client 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1,112 +1,9 @@
1
- /**
2
- * Parse a {@link Schemas.SparqlTextResponse} (whose `results` field carries the
3
- * SPARQL Results document as a JSON *string*) into typed bindings plus flat
4
- * `{ variable: lexicalValue }` rows — the form most callers want, so they never
5
- * have to `JSON.parse` and zip `head.vars` with binding values by hand.
6
- */
7
- export function parseSparqlResults(response) {
8
- const doc = JSON.parse(response.results);
9
- const vars = doc.head?.vars ?? [];
10
- if (typeof doc.boolean === "boolean") {
11
- return { vars, boolean: doc.boolean, bindings: [], rows: [] };
12
- }
13
- const bindings = doc.results?.bindings ?? [];
14
- const rows = bindings.map((binding) => Object.fromEntries(Object.entries(binding).map(([name, term]) => [name, term.value])));
15
- return { vars, boolean: null, bindings, rows };
16
- }
17
- function firstPatternVariable(patterns) {
18
- for (const pattern of patterns) {
19
- if ("var" in pattern.subject)
20
- return pattern.subject.var;
21
- if ("var" in pattern.object)
22
- return pattern.object.var;
23
- }
24
- return "entity";
25
- }
26
- function attributeFilterValue(value) {
27
- if (typeof value === "boolean")
28
- return { bool: value };
29
- if (typeof value === "number")
30
- return Number.isInteger(value) ? { i64: value } : { f64: value };
31
- if (typeof value === "string")
32
- return { str: value };
33
- if ("dateTime" in value)
34
- return { date_time: value.dateTime };
35
- return { entity: value.entity };
36
- }
37
- function attributeFilter(filter, defaultVar) {
38
- return {
39
- compare: {
40
- op: filter.op ?? "eq",
41
- left: { property: { var: filter.var ?? defaultVar, field: filter.field } },
42
- right: { value: attributeFilterValue(filter.value) },
43
- },
44
- };
45
- }
46
- /** Thrown when the server responds with a non-2xx status. */
47
- export class LbbError extends Error {
48
- status;
49
- body;
50
- error;
51
- type;
52
- code;
53
- param;
54
- requestId;
55
- docUrl;
56
- constructor(status, body, error) {
57
- super(error?.message ?? `Little Big Brain ${status}: ${body}`);
58
- this.status = status;
59
- this.body = body;
60
- this.error = error;
61
- this.name = "LbbError";
62
- this.type = error?.type;
63
- this.code = error?.code;
64
- this.param = error?.param;
65
- this.requestId = error?.request_id;
66
- this.docUrl = error?.doc_url;
67
- }
68
- }
69
- function sleep(ms) {
70
- if (ms <= 0)
71
- return Promise.resolve();
72
- const timer = globalThis
73
- .setTimeout;
74
- return new Promise((resolve) => {
75
- if (timer) {
76
- timer(resolve, ms);
77
- }
78
- else {
79
- resolve();
80
- }
81
- });
82
- }
83
- function retryableStatus(status) {
84
- return status === 429 || status >= 500;
85
- }
86
- function retryAllowed(method, idempotencyKey) {
87
- const upper = method.toUpperCase();
88
- return upper === "GET" || upper === "HEAD" || upper === "OPTIONS" || idempotencyKey !== undefined;
89
- }
90
- function parseLbbError(status, body, fallbackRequestId) {
91
- try {
92
- const parsed = JSON.parse(body);
93
- if (parsed.error) {
94
- return new LbbError(status, body, {
95
- ...parsed.error,
96
- request_id: parsed.error.request_id ?? fallbackRequestId ?? null,
97
- });
98
- }
99
- }
100
- catch {
101
- // Fall through to an unstructured error.
102
- }
103
- return new LbbError(status, body, {
104
- type: "api_error",
105
- code: "unstructured_error",
106
- message: body || `Little Big Brain ${status}`,
107
- request_id: fallbackRequestId ?? null,
108
- });
109
- }
1
+ import { parseSparqlResults } from "./types.js";
2
+ import { parseLbbError, parseResponseJson, retryAllowed, retryDelayForAttempt, retryableStatus, sleep, } from "./transport.js";
3
+ import { AdminNamespace, ContextNamespace, EntityNamespace, GraphNamespace, IndexNamespace, OntologyNamespace, QueryNamespace, SchemaNamespace, SearchNamespace, } from "./namespaces.js";
4
+ export { parseSparqlResults } from "./types.js";
5
+ export { LbbError } from "./transport.js";
6
+ export { AdminNamespace, ContextNamespace, EntityNamespace, FactsNamespace, GraphNamespace, IndexNamespace, OntologyNamespace, QueryNamespace, SchemaNamespace, SearchNamespace, } from "./namespaces.js";
110
7
  /**
111
8
  * A typed HTTP client for a Little Big Brain graph server. One instance is scoped to a
112
9
  * single graph/branch; construct another for a different scope. All methods
@@ -122,10 +19,17 @@ export class LbbClient {
122
19
  apiVersion;
123
20
  maxRetries;
124
21
  retryDelayMs;
22
+ timeoutMs;
23
+ onRequest;
24
+ onResponse;
25
+ admin;
26
+ context;
125
27
  search;
126
28
  indexes;
127
29
  entities;
128
30
  schema;
31
+ ontology;
32
+ query;
129
33
  constructor(options) {
130
34
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
131
35
  this.apiKey = options.apiKey;
@@ -135,16 +39,32 @@ export class LbbClient {
135
39
  this.apiVersion = options.apiVersion ?? "2026-06-22";
136
40
  this.maxRetries = options.maxRetries ?? 2;
137
41
  this.retryDelayMs = options.retryDelayMs ?? 100;
42
+ this.timeoutMs = options.timeoutMs ?? 120_000;
43
+ this.onRequest = options.onRequest;
44
+ this.onResponse = options.onResponse;
45
+ if (!Number.isInteger(this.maxRetries) || this.maxRetries < 0) {
46
+ throw new RangeError("maxRetries must be a non-negative integer");
47
+ }
48
+ if (!Number.isFinite(this.retryDelayMs) || this.retryDelayMs < 0) {
49
+ throw new RangeError("retryDelayMs must be a non-negative number");
50
+ }
51
+ if (!Number.isFinite(this.timeoutMs) || this.timeoutMs < 0) {
52
+ throw new RangeError("timeoutMs must be a non-negative number");
53
+ }
138
54
  const fallback = globalThis.fetch;
139
55
  const chosen = options.fetch ?? (fallback ? fallback.bind(globalThis) : undefined);
140
56
  if (!chosen) {
141
57
  throw new Error("no fetch implementation available; pass options.fetch");
142
58
  }
143
59
  this.fetchImpl = chosen;
60
+ this.admin = new AdminNamespace(this);
61
+ this.context = new ContextNamespace(this);
144
62
  this.search = new SearchNamespace(this);
145
63
  this.indexes = new IndexNamespace(this);
146
64
  this.entities = new EntityNamespace(this);
147
65
  this.schema = new SchemaNamespace(this);
66
+ this.ontology = new OntologyNamespace(this);
67
+ this.query = new QueryNamespace(this);
148
68
  }
149
69
  graph(name, opts = {}) {
150
70
  return new GraphNamespace(this.withScope({
@@ -169,6 +89,9 @@ export class LbbClient {
169
89
  apiVersion: this.apiVersion,
170
90
  maxRetries: this.maxRetries,
171
91
  retryDelayMs: this.retryDelayMs,
92
+ timeoutMs: this.timeoutMs,
93
+ onRequest: this.onRequest,
94
+ onResponse: this.onResponse,
172
95
  });
173
96
  }
174
97
  buildUrl(path, query) {
@@ -196,7 +119,8 @@ export class LbbClient {
196
119
  headers["authorization"] = `Bearer ${this.apiKey}`;
197
120
  if (opts.idempotencyKey !== undefined)
198
121
  headers["idempotency-key"] = opts.idempotencyKey;
199
- const canRetry = retryAllowed(method, opts.idempotencyKey);
122
+ Object.assign(headers, opts.headers ?? {});
123
+ const canRetry = opts.retry ?? retryAllowed(method, opts.idempotencyKey);
200
124
  const body = opts.rawBody !== undefined
201
125
  ? opts.rawBody
202
126
  : opts.body !== undefined
@@ -207,40 +131,99 @@ export class LbbClient {
207
131
  headers,
208
132
  body,
209
133
  };
134
+ const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
135
+ const maxRetries = opts.maxRetries ?? this.maxRetries;
136
+ if (!Number.isInteger(maxRetries) || maxRetries < 0) {
137
+ throw new RangeError("maxRetries must be a non-negative integer");
138
+ }
139
+ const startedAt = Date.now();
140
+ const url = this.buildUrl(path, opts.query);
141
+ let attempts = 0;
210
142
  let response;
211
143
  let text = "";
212
- for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
144
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
145
+ if (opts.signal?.aborted)
146
+ throw opts.signal.reason ?? new Error("request aborted");
147
+ attempts = attempt + 1;
148
+ const controller = (timeoutMs > 0 || opts.signal) && typeof AbortController !== "undefined"
149
+ ? new AbortController()
150
+ : undefined;
151
+ const abortFromCaller = () => controller?.abort(opts.signal?.reason);
152
+ opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
153
+ const timer = controller
154
+ ? timeoutMs > 0
155
+ ? setTimeout(() => controller.abort(), timeoutMs)
156
+ : undefined
157
+ : undefined;
213
158
  try {
214
- response = await this.fetchImpl(this.buildUrl(path, opts.query), init);
159
+ this.onRequest?.({
160
+ method: method.toUpperCase(),
161
+ url,
162
+ attempt: attempts,
163
+ maxAttempts: maxRetries + 1,
164
+ idempotencyKey: opts.idempotencyKey,
165
+ });
166
+ response = await this.fetchImpl(url, {
167
+ ...init,
168
+ signal: controller?.signal ?? opts.signal,
169
+ });
215
170
  text = await response.text();
216
171
  }
217
172
  catch (error) {
218
- if (canRetry && attempt < this.maxRetries) {
173
+ const callerAborted = opts.signal?.aborted === true;
174
+ const requestError = controller?.signal.aborted && !callerAborted
175
+ ? Object.assign(new Error(`Little Big Brain request timed out after ${timeoutMs}ms`, {
176
+ cause: error,
177
+ }), { name: "TimeoutError" })
178
+ : error;
179
+ if (!callerAborted && canRetry && attempt < maxRetries) {
219
180
  await sleep(this.retryDelayMs * (attempt + 1));
220
181
  continue;
221
182
  }
222
- throw error;
183
+ throw requestError;
184
+ }
185
+ finally {
186
+ if (timer !== undefined)
187
+ clearTimeout(timer);
188
+ opts.signal?.removeEventListener("abort", abortFromCaller);
223
189
  }
224
- if (response.ok || !retryableStatus(response.status) || attempt === this.maxRetries) {
190
+ if (response.ok ||
191
+ !retryableStatus(response.status) ||
192
+ attempt === maxRetries) {
225
193
  break;
226
194
  }
227
195
  if (!canRetry) {
228
196
  break;
229
197
  }
230
- await sleep(this.retryDelayMs * (attempt + 1));
198
+ await sleep(retryDelayForAttempt(this.retryDelayMs, attempt, response.headers?.get("retry-after")));
231
199
  }
232
200
  if (response === undefined)
233
201
  throw new Error("request did not produce a response");
234
202
  const requestId = response.headers?.get("x-request-id") ?? undefined;
235
203
  const version = response.headers?.get("lbb-version") ?? undefined;
204
+ const elapsedMs = Math.max(0, Date.now() - startedAt);
205
+ this.onResponse?.({
206
+ method: method.toUpperCase(),
207
+ url,
208
+ status: response.status,
209
+ requestId,
210
+ attempts,
211
+ retryCount: Math.max(0, attempts - 1),
212
+ elapsedMs,
213
+ });
236
214
  if (!response.ok)
237
215
  throw parseLbbError(response.status, text.trim(), requestId);
238
216
  return {
239
- data: (text ? JSON.parse(text) : undefined),
217
+ data: text
218
+ ? parseResponseJson(text, response.status, requestId)
219
+ : undefined,
240
220
  status: response.status,
241
221
  requestId,
242
222
  version,
243
223
  headers: response.headers,
224
+ attempts,
225
+ retryCount: Math.max(0, attempts - 1),
226
+ elapsedMs,
244
227
  };
245
228
  }
246
229
  async request(method, path, opts = {}) {
@@ -281,6 +264,13 @@ export class LbbClient {
281
264
  * bounded internal commits server-side, so a whole dataset loads in one
282
265
  * streamed request without a single oversized commit. Pass `lines` as an array
283
266
  * (serialized to NDJSON here) or a pre-built NDJSON string.
267
+ *
268
+ * Set `index: true` to run one full index build after the last batch, so the
269
+ * data is served from the persisted runs (not just the ephemeral snapshot
270
+ * fallback) by the time the call returns — the "bulk load, queryable on return"
271
+ * path. Prefer this over indexing per batch (which serializes builds and races
272
+ * the throttle): import the whole dataset, index once. The response's `index`
273
+ * object reports whether the build ran or was skipped.
284
274
  */
285
275
  import(lines, opts = {}) {
286
276
  const ndjson = typeof lines === "string"
@@ -289,8 +279,13 @@ export class LbbClient {
289
279
  return this.request("POST", "/v1/graph/import", {
290
280
  rawBody: ndjson,
291
281
  contentType: "application/x-ndjson",
292
- query: { batch: opts.batch, strict: opts.strict, observed_at: opts.observedAt },
293
- idempotencyKey: opts.idempotencyKey,
282
+ query: {
283
+ batch: opts.batch,
284
+ strict: opts.strict,
285
+ observed_at: opts.observedAt,
286
+ index: opts.index,
287
+ },
288
+ idempotencyKey: opts.idempotencyKey ?? this.idempotencyKey("import"),
294
289
  });
295
290
  }
296
291
  /**
@@ -310,7 +305,7 @@ export class LbbClient {
310
305
  resource_type: opts.resourceType,
311
306
  edge_idempotency: opts.edgeIdempotency,
312
307
  },
313
- idempotencyKey: opts.idempotencyKey,
308
+ idempotencyKey: opts.idempotencyKey ?? this.idempotencyKey("import-rdf"),
314
309
  });
315
310
  }
316
311
  /**
@@ -333,13 +328,200 @@ export class LbbClient {
333
328
  createBranch(body) {
334
329
  return this.request("POST", "/v1/graph/branch", { body });
335
330
  }
331
+ /**
332
+ * WS16 validate-then-merge: replay `from_branch`'s post-fork commits onto the
333
+ * SCOPED branch (its fork parent) as one new commit. A write — sends an
334
+ * Idempotency-Key so a retry replays instead of re-applying.
335
+ */
336
+ mergeBranch(body, opts = {}) {
337
+ return this.request("POST", "/v1/graph/branch/merge", {
338
+ body,
339
+ idempotencyKey: opts.idempotencyKey ?? this.idempotencyKey("branch-merge"),
340
+ });
341
+ }
342
+ /**
343
+ * WS15 observe: store a conversation episode verbatim as EPISODE evidence,
344
+ * anchor + gate extracted facts on an observe branch, and optionally
345
+ * auto-merge when validation is clean. Flag-gated server-side
346
+ * (`--enable-observe`). A write — carries an Idempotency-Key.
347
+ */
348
+ observe(body, opts = {}) {
349
+ return this.request("POST", "/v1/memory/observe", {
350
+ body,
351
+ idempotencyKey: opts.idempotencyKey ?? this.idempotencyKey("observe"),
352
+ });
353
+ }
336
354
  /**
337
355
  * Delete every object under the scoped graph/branch — a destructive reset.
338
356
  * `confirm` must equal the scoped graph id; the next commit re-initializes the
339
357
  * graph. Branch-scoped: sibling branches are untouched.
340
358
  */
341
359
  deleteGraph(opts) {
342
- return this.request("POST", "/v1/graph/delete", { query: { confirm: opts.confirm } });
360
+ return this.request("POST", "/v1/graph/delete", {
361
+ query: { confirm: opts.confirm },
362
+ });
363
+ }
364
+ // --- models as runs (WS9 registry + eval machinery) ---
365
+ /**
366
+ * The graph's grounding vocabulary as byte-sorted, deduped string sections —
367
+ * the canonical input for a decoder-side automaton (FST/trie) and the
368
+ * vocabulary half of an export bundle.
369
+ */
370
+ vocabExport(opts = {}) {
371
+ return this.request("GET", "/v1/search/vocab", {
372
+ query: { sections: opts.sections?.join(","), limit: opts.limit },
373
+ });
374
+ }
375
+ /**
376
+ * Captured signals by flush-seq range, oldest first — the flywheel training
377
+ * feed. The `seq` on each signal is the temporal-split coordinate (train ≤ T,
378
+ * eval > T).
379
+ */
380
+ readSignals(opts = {}) {
381
+ return this.request("GET", "/v1/signals", {
382
+ query: { from: opts.from, to: opts.to, limit: opts.limit },
383
+ });
384
+ }
385
+ /**
386
+ * Record one immutable model-as-run manifest; runs number sequentially per
387
+ * kind. Trainers MUST train on data ≤ `trained_at_commit_seq` and evaluate
388
+ * past it — `modelSplitAudit` verifies the recorded lineage.
389
+ */
390
+ recordModelRun(body) {
391
+ return this.request("POST", "/v1/models/record", { body });
392
+ }
393
+ /** CAS-promote a recorded run to CURRENT for its kind (replay is a no-op). */
394
+ promoteModelRun(opts) {
395
+ return this.request("POST", "/v1/models/promote", {
396
+ query: { kind: opts.kind, run: opts.run },
397
+ });
398
+ }
399
+ /** A kind's model runs, newest first, with effective promotion state. */
400
+ modelRegistry(opts) {
401
+ return this.request("GET", "/v1/models/registry", {
402
+ query: { kind: opts.kind },
403
+ });
404
+ }
405
+ /** GC run prefixes beyond the promoted run + the last `keep`; reports deletions. */
406
+ modelRegistryGc(opts) {
407
+ return this.request("POST", "/v1/models/registry/gc", {
408
+ query: { kind: opts.kind, keep: opts.keep },
409
+ });
410
+ }
411
+ /** Verify a run's temporal-split obligation from its recorded lineage. */
412
+ modelSplitAudit(opts) {
413
+ return this.request("GET", "/v1/models/split-audit", {
414
+ query: { kind: opts.kind, run: opts.run },
415
+ });
416
+ }
417
+ /**
418
+ * Champion vs challenger retrieval over one pinned snapshot. Returns
419
+ * promotion evidence (hit-rate@k, latency, overlap); never promotes.
420
+ */
421
+ shadowEval(body) {
422
+ return this.request("POST", "/v1/models/shadow-eval", { body });
423
+ }
424
+ /**
425
+ * Execution-verified QA probes generated from the graph's current edges —
426
+ * labels are the executed projections, so they are verified by construction.
427
+ * Feeds `shadowEval` directly.
428
+ */
429
+ syntheticEval(opts = {}) {
430
+ return this.request("GET", "/v1/models/synthetic-eval", {
431
+ query: { limit: opts.limit },
432
+ });
433
+ }
434
+ /** The doubling retrain policy: is a retrain due for this model kind? */
435
+ modelCadence(opts) {
436
+ return this.request("GET", "/v1/models/cadence", {
437
+ query: { kind: opts.kind },
438
+ });
439
+ }
440
+ /**
441
+ * One deterministic trainer tick: build a probe set (execution-verified
442
+ * synthetic pairs, or bring your own), search a bounded candidate space on
443
+ * the train slice, gate the winner against the champion on the held-out
444
+ * eval slice, record the run either way, and promote only when the gate
445
+ * passes. The same tick the `auto_train` cadence fires — always safe to
446
+ * call by hand.
447
+ */
448
+ trainTick(body) {
449
+ return this.request("POST", "/v1/models/train-tick", { body });
450
+ }
451
+ /** The graph's automatic-training configuration (default: off). */
452
+ trainingConfig() {
453
+ return this.request("GET", "/v1/models/training-config", {});
454
+ }
455
+ /** Set the automatic-training configuration (`auto_train` toggle + kinds). */
456
+ setTrainingConfig(body) {
457
+ return this.request("POST", "/v1/models/training-config", { body });
458
+ }
459
+ /**
460
+ * Verdict on an ask (`accepted` | `rejected` | `corrected` + the right
461
+ * plan), joined to the ask's trace by `ask_id` — the planner fine-tune's
462
+ * explicit feedback capture. `accepted: false` in the response means
463
+ * signal capture is off on this deployment (the contract is identical).
464
+ */
465
+ askFeedback(body) {
466
+ return this.request("POST", "/v1/ask/feedback", { body });
467
+ }
468
+ /**
469
+ * The planner fine-tune's training feed: accepted/corrected feedback
470
+ * joined to its traces (signals ≤ the split pin), topped up with
471
+ * execution-verified synthetic plans.
472
+ */
473
+ plannerDataset(opts = {}) {
474
+ return this.request("GET", "/v1/models/planner-dataset", {
475
+ query: { limit: opts.limit, split_seq: opts.splitSeq },
476
+ });
477
+ }
478
+ /**
479
+ * The DPO pass's training feed: preference pairs from corrected verdicts,
480
+ * paired rejections, and synthetic corrupted-slot pairs.
481
+ */
482
+ plannerPreferenceDataset(opts = {}) {
483
+ return this.request("GET", "/v1/models/planner-preference-dataset", {
484
+ query: { limit: opts.limit, split_seq: opts.splitSeq },
485
+ });
486
+ }
487
+ /**
488
+ * The suggest-ranker trainer's probe feed: `suggestion_adopted` signals
489
+ * (typed prefix + adopted text) ≤ the split pin, topped up with
490
+ * execution-verified synthetic vocabulary pairs.
491
+ */
492
+ suggestDataset(opts = {}) {
493
+ return this.request("GET", "/v1/models/suggest-dataset", {
494
+ query: { limit: opts.limit, split_seq: opts.splitSeq },
495
+ });
496
+ }
497
+ /**
498
+ * The extractor fine-tune's training feed: EPISODE transcripts joined to
499
+ * the facts the observe pipeline committed from them.
500
+ */
501
+ extractorDataset(opts = {}) {
502
+ return this.request("GET", "/v1/models/extractor-dataset", {
503
+ query: { limit: opts.limit, split_seq: opts.splitSeq },
504
+ });
505
+ }
506
+ /**
507
+ * Promote a finished `extractor_lora` training run: gated on held-out fact
508
+ * F1, recorded as a WS9 `kind=extractor` run whose adapter resident
509
+ * extraction then serves.
510
+ */
511
+ promoteExtractor(opts) {
512
+ return this.request("POST", "/v1/models/promote-extractor", {
513
+ query: { run_id: opts.runId, allow_regression: opts.allowRegression },
514
+ });
515
+ }
516
+ /**
517
+ * Promote a finished `planner_lora` training run: gated on held-out slot
518
+ * exactness, recorded as a WS9 `kind=planner` run whose adapter `/v1/ask`
519
+ * then serves.
520
+ */
521
+ promotePlanner(opts) {
522
+ return this.request("POST", "/v1/models/promote-planner", {
523
+ query: { run_id: opts.runId, allow_regression: opts.allowRegression },
524
+ });
343
525
  }
344
526
  // --- search ---
345
527
  /** Full semantic hybrid search from a request body (`POST /v1/graph/search`). */
@@ -350,6 +532,47 @@ export class LbbClient {
350
532
  multiSearch(body) {
351
533
  return this.request("POST", "/v1/search/multi", { body });
352
534
  }
535
+ /**
536
+ * Grounded prefix completion from the index vocabulary + ontology. Optionally
537
+ * narrow relation completions by a type-signature `context` (WS10) — a type
538
+ * pair that admits a single relation flags `signature_forced`.
539
+ */
540
+ suggest(body) {
541
+ return this.request("POST", "/v1/search/suggest", { body });
542
+ }
543
+ /**
544
+ * Snap free text to the nearest real vocabulary item (WS11). Embedding cosine
545
+ * on a managed graph, else lexical; never fabricates a term.
546
+ */
547
+ resolveTerm(body) {
548
+ return this.request("POST", "/v1/search/resolve-term", { body });
549
+ }
550
+ /**
551
+ * Ground a natural-language question to the graph's real vocabulary, retrieve
552
+ * against the pinned snapshot, and answer with citations (WS12, `/v1/ask`).
553
+ */
554
+ ask(body) {
555
+ return this.request("POST", "/v1/ask", { body });
556
+ }
557
+ /**
558
+ * Name the relation between two entities (`/v1/decode`): the DB narrows the
559
+ * candidates to the type pair's admissible relations (WS10), answers alone
560
+ * when the pair forces a single relation, and otherwise decodes it with the
561
+ * graph-native fine-tuned model — the "DB narrows, cheap model decodes" call.
562
+ */
563
+ decode(body) {
564
+ return this.request("POST", "/v1/decode", { body });
565
+ }
566
+ /**
567
+ * Report which completion mechanisms will carry on this graph (WS13):
568
+ * signature sparsity, name semantics, sampled narrowing recall, and a
569
+ * narrow / narrow+finetune / lexical-first recommendation.
570
+ */
571
+ groundability(opts = {}) {
572
+ return this.request("GET", "/v1/graph/groundability", {
573
+ query: opts.sample != null ? { sample: String(opts.sample) } : undefined,
574
+ });
575
+ }
353
576
  /**
354
577
  * Append relevance labels for a set of search results — how Little Big Brain
355
578
  * gathers customer-specific qrels. Grade results (3 ideal/good, 1 partial,
@@ -609,7 +832,9 @@ export class LbbClient {
609
832
  * timeout (a 504), then poll `metadata()` for completion.
610
833
  */
611
834
  indexBuild(opts = {}) {
612
- return this.request("POST", "/v1/index/build", { query: { background: opts.background || undefined } });
835
+ return this.request("POST", "/v1/index/build", {
836
+ query: { background: opts.background || undefined },
837
+ });
613
838
  }
614
839
  /**
615
840
  * Build BM25, ANN/vector, and adjacency index families. With
@@ -618,7 +843,9 @@ export class LbbClient {
618
843
  * exceed a fronting gateway's timeout, then poll `metadata()` for completion.
619
844
  */
620
845
  indexRun(opts = {}) {
621
- return this.request("POST", "/v1/index/run", { query: { background: opts.background || undefined } });
846
+ return this.request("POST", "/v1/index/run", {
847
+ query: { background: opts.background || undefined },
848
+ });
622
849
  }
623
850
  /** Append a BM25 delta segment for the unindexed WAL tail. */
624
851
  indexDelta() {
@@ -633,7 +860,10 @@ export class LbbClient {
633
860
  /** Fold the WAL tail into snapshot segments. */
634
861
  compact(opts = {}) {
635
862
  return this.request("POST", "/v1/graph/compact", {
636
- query: { min_tail_commits: opts.minTailCommits, max_segments: opts.maxSegments },
863
+ query: {
864
+ min_tail_commits: opts.minTailCommits,
865
+ max_segments: opts.maxSegments,
866
+ },
637
867
  });
638
868
  }
639
869
  // --- inspection ---
@@ -664,215 +894,35 @@ export class LbbClient {
664
894
  }
665
895
  /** Rotate a database stack key and return the new one-time API key. */
666
896
  adminRotateStackKey(slug) {
667
- return this.request("POST", "/api/admin/stacks/rotate-key", { query: { stack: slug } });
897
+ return this.request("POST", "/api/admin/stacks/rotate-key", {
898
+ query: { stack: slug },
899
+ });
668
900
  }
669
901
  /** Delete a database stack after confirming the slug. */
670
902
  adminDeleteStack(slug) {
671
- return this.request("DELETE", "/api/admin/stacks", { query: { stack: slug, confirm: slug } });
903
+ return this.request("DELETE", "/api/admin/stacks", {
904
+ query: { stack: slug, confirm: slug },
905
+ });
672
906
  }
673
907
  /**
674
908
  * Mint a short-lived `lbb_ses_…` session token for an account. A trusted
675
909
  * co-located service uses it (with `?stack=<slug>`) to call the data plane on
676
910
  * the account's behalf without handling the stack's mode-bearing stack key.
677
911
  */
678
- adminMintSession(accountId) {
679
- return this.request("POST", "/api/admin/sessions", { body: { account_id: accountId } });
912
+ adminMintSession(accountId, ttlSeconds) {
913
+ const body = { account_id: accountId };
914
+ if (ttlSeconds !== undefined)
915
+ body.ttl_seconds = ttlSeconds;
916
+ return this.request("POST", "/api/admin/sessions", { body });
680
917
  }
681
918
  /** Customer-visible activity for one database stack. */
682
919
  adminStackActivity(slug, window = "24h") {
683
- return this.request("GET", "/api/admin/stacks/activity", { query: { stack: slug, window } });
920
+ return this.request("GET", "/api/admin/stacks/activity", {
921
+ query: { stack: slug, window },
922
+ });
684
923
  }
685
924
  /** Activity for the stack selected by the bearer stack key or session. */
686
925
  stackActivity(window = "24h") {
687
926
  return this.request("GET", "/v1/stack/activity", { query: { window } });
688
927
  }
689
928
  }
690
- export class GraphNamespace {
691
- client;
692
- facts;
693
- constructor(client) {
694
- this.client = client;
695
- this.facts = new FactsNamespace(client);
696
- }
697
- branch(name) {
698
- return new GraphNamespace(this.client.withScope({ branch: name }));
699
- }
700
- create() {
701
- return this.client.createGraph();
702
- }
703
- delete(opts) {
704
- return this.client.deleteGraph(opts);
705
- }
706
- /** Retract edges/entities from the scoped graph. See {@link LbbClient.retract}. */
707
- retract(body, opts = {}) {
708
- return this.client.retract(body, opts);
709
- }
710
- }
711
- export class FactsNamespace {
712
- client;
713
- constructor(client) {
714
- this.client = client;
715
- }
716
- create(body, opts = {}) {
717
- return this.client.request("POST", "/v1/graph/commit", {
718
- body,
719
- idempotencyKey: opts.idempotencyKey ?? this.client.idempotencyKey("facts.create"),
720
- });
721
- }
722
- /** Bulk-load a dataset as NDJSON. See {@link LbbClient.import}. */
723
- import(lines, opts = {}) {
724
- return this.client.import(lines, opts);
725
- }
726
- /**
727
- * Bulk-load N-Triples through the native RDF import endpoint.
728
- *
729
- * Statements are committed through the fixed RDF_TRIPLE relation; source RDF
730
- * predicates and literal term details are preserved as edge metadata.
731
- */
732
- importRdf(ntriples, opts = {}) {
733
- return this.client.importRdf(ntriples, opts);
734
- }
735
- }
736
- export class SearchNamespace {
737
- client;
738
- constructor(client) {
739
- this.client = client;
740
- }
741
- hybrid(input, opts = {}) {
742
- if (typeof input !== "string") {
743
- return this.client.request("POST", "/v1/graph/search", { body: input });
744
- }
745
- return this.client.request("GET", "/v1/search", {
746
- query: {
747
- query: input,
748
- top_k: opts.topK,
749
- source: opts.source,
750
- consistency: opts.consistency,
751
- lexical: opts.lexical,
752
- bm25: opts.bm25,
753
- vector: opts.vector,
754
- targets: opts.targets?.join(","),
755
- profile: opts.profile,
756
- log_impression: opts.logImpression,
757
- },
758
- });
759
- }
760
- multi(body) {
761
- return this.client.multiSearch(body);
762
- }
763
- feedback(body, opts = {}) {
764
- return this.client.searchFeedback(body, opts);
765
- }
766
- feedbackExport() {
767
- return this.client.searchFeedbackExport();
768
- }
769
- fullText(body) {
770
- return this.client.fullTextSearch(body);
771
- }
772
- vector(body) {
773
- return this.client.embeddingSearch(body);
774
- }
775
- }
776
- export class SchemaNamespace {
777
- client;
778
- constructor(client) {
779
- this.client = client;
780
- }
781
- /** Active graph schema bundle: ontology plus activated SHACL shapes. */
782
- view(opts = {}) {
783
- return this.client.request("GET", "/v1/schema", {
784
- query: { audit: opts.audit || undefined },
785
- });
786
- }
787
- /** Preview a proposed RDF/SHACL schema bundle and audit current data. */
788
- preview(body) {
789
- return this.client.request("POST", "/v1/schema/preview", { body });
790
- }
791
- /** Activate a previewed SHACL schema bundle for this graph branch. */
792
- publish(body) {
793
- return this.client.request("POST", "/v1/schema/publish", { body });
794
- }
795
- /** Audit current data against the active SHACL schema bundle. */
796
- audit() {
797
- return this.client.request("POST", "/v1/schema/audit");
798
- }
799
- }
800
- export class IndexNamespace {
801
- client;
802
- constructor(client) {
803
- this.client = client;
804
- }
805
- run(opts = {}) {
806
- const background = opts.background ?? (opts.wait === false ? true : undefined);
807
- return this.client.request("POST", "/v1/index/run", {
808
- query: { background },
809
- body: opts.body,
810
- });
811
- }
812
- build() {
813
- return this.client.indexBuild();
814
- }
815
- delta() {
816
- return this.client.indexDelta();
817
- }
818
- gc(opts = {}) {
819
- return this.client.indexGc(opts);
820
- }
821
- }
822
- export class EntityNamespace {
823
- client;
824
- constructor(client) {
825
- this.client = client;
826
- }
827
- /**
828
- * Browse entities as the unified list envelope. Pass `fields` (names or `*`)
829
- * to inline each row's typed attributes as native JSON (under `attributes`) —
830
- * "list entities and their titles" in one call instead of a list plus N point
831
- * lookups — or `ids`
832
- * to fetch a specific set. Page with `cursor` from the previous `next_cursor`.
833
- */
834
- list(opts = {}) {
835
- const csv = (v) => Array.isArray(v) ? v.join(",") : v;
836
- return this.client.request("GET", "/v1/graph/entities", {
837
- query: {
838
- type: opts.type,
839
- limit: opts.limit,
840
- cursor: opts.cursor,
841
- offset: opts.offset,
842
- q: opts.query,
843
- fields: csv(opts.fields),
844
- ids: csv(opts.ids),
845
- },
846
- });
847
- }
848
- get(opts) {
849
- return this.client.entityMetadata(opts);
850
- }
851
- detail(opts) {
852
- return this.client.entityDetail(opts);
853
- }
854
- /**
855
- * Filter entities already bound by relation patterns using typed attributes,
856
- * without writing RDF property IRIs by hand. This is a convenience wrapper over
857
- * the structured SPARQL route: relation `patterns` bind variables, and `where`
858
- * compares ontology property fields on those bound variables.
859
- */
860
- filterByAttributes(opts) {
861
- const defaultVar = firstPatternVariable(opts.patterns);
862
- const where = Array.isArray(opts.where) ? opts.where : [opts.where];
863
- return this.client.sparql({
864
- patterns: opts.patterns,
865
- filters: [...(opts.filters ?? []), ...where.map((filter) => attributeFilter(filter, defaultVar))],
866
- select: opts.select,
867
- limit: opts.limit,
868
- offset: opts.offset,
869
- as_of_valid_time: opts.asOfValidTime,
870
- as_of_commit_seq: opts.asOfCommitSeq,
871
- order_by: opts.orderBy,
872
- reason: opts.reason,
873
- max_solutions: opts.maxSolutions,
874
- max_object_reads: opts.maxObjectReads,
875
- max_fetched_bytes: opts.maxFetchedBytes,
876
- });
877
- }
878
- }