@littlebigbrain/mcp 0.5.1 → 0.6.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/tools.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { metadataPage } from "./metadata-pages.js";
3
3
  import { registerRdfTool } from "./rdf-tool.js";
4
- import { IDEMPOTENT_WRITE, MUTATING, READ_ONLY, configureInputSchema, configureWireSchema, detailSchema, graphScope, inspectInputSchema, inspectWireSchema, jsonObjectSchema, queryInputSchema, queryWireSchema, searchFeedbackSchema, } from "./tool-contracts.js";
5
- import { analyze, assertCursorScope, contentHashKey, decodeQueryCursor, effectiveRowLimit, enrichError, errorResult, guide, normalizeDetail, normalizeLbbIris, ontologyDefineBody, queryCommitPin, queryEnvelope, requireString, rowPageFrom, rowPageNext, run, scoped, stableJson, toolResult, } from "./tool-runtime.js";
6
- export function registerLbbTools(server, client) {
4
+ import { queryTiming } from "./query-observer.js";
5
+ import { DESTRUCTIVE, IDEMPOTENT_WRITE, MUTATING, READ_ONLY, configureInputSchema, configureWireSchema, detailSchema, graphScope, inspectInputSchema, inspectWireSchema, jsonObjectSchema, queryInputSchema, queryWireSchema, searchFeedbackSchema, } from "./tool-contracts.js";
6
+ import { analyze, assertCursorScope, contentHashKey, decodeQueryCursor, effectiveRowLimit, enrichError, errorResult, guide, normalizeDetail, normalizeLbbIris, ontologyDefineBody, queryCommitPin, queryEnvelope, queryToolResult, requireString, rowPageFrom, rowPageNext, run, scoped, stableJson, toolResult, } from "./tool-runtime.js";
7
+ export function registerLbbTools(server, client, options = {}) {
7
8
  registerRdfTool(server, client);
8
9
  server.registerTool("lbb_inspect", {
9
10
  description: "Read graph context and exact graph facts. Actions: guide, graphs, publication, ontology, ontology_conformance, schema, ontology_search, metadata, entity, state, history, transitions, why. graphs works before bootstrap; publication reports whether writes are queryable. ontology and schema return complete entries with page_size, section and cursor; follow next until absent. schema reads active native ontology/SHACL metadata without running validation. Query asserted RDF/OWL axioms separately with lbb_query. ontology_conformance serves the durable report referenced by the pinned published root. entity returns one node's metadata, scalar attributes, bounded Base-backed edge neighborhood, history, and observations. Use lbb_query with SPARQL property paths for precise path selection.",
@@ -20,7 +21,7 @@ export function registerLbbTools(server, client) {
20
21
  .catch(async (error) => errorResult(await enrichError(client, error)));
21
22
  }
22
23
  return run(client, `lbb_inspect.${args.action}`, args.detail, () => {
23
- const target = scoped(client, args.graph, args.branch);
24
+ const target = scoped(client, args.graph);
24
25
  switch (args.action) {
25
26
  case "guide":
26
27
  return guide(target);
@@ -94,7 +95,7 @@ export function registerLbbTools(server, client) {
94
95
  });
95
96
  });
96
97
  server.registerTool("lbb_query", {
97
- description: "Analytical and expert reads. Modes: structured (SPARQL-subset JSON body), sparql (SPARQL text), analyze. SPARQL is the only query surface. Relations are <https://littlebigbrain.com/r/NAME> and types <https://littlebigbrain.com/class/NAME> (both lowercased); entities are content-addressed, so anchor a named one by its rdfs:label rather than building its IRI. Structured and text queries pin one published watermark for the request.",
98
+ description: "Analytical and expert reads. Modes: structured (SPARQL-subset JSON body), sparql (SPARQL text), search (instances by meaning over every searchable class; filter narrows by class and relationship; every hit checked against the graph), analyze. SPARQL is the query language; search finds what the words describe. To plan a search: lbb_embeddings action=list, then SPARQL for a class's relationships on a sample, then mode=search with explain=true to check the resolved filter before the real search (lbb_inspect action=guide has the queries). Relations are <https://littlebigbrain.com/r/NAME> and types <https://littlebigbrain.com/class/NAME> (both lowercased); entities are content-addressed, so anchor a named one by its rdfs:label rather than building its IRI. Structured and text queries pin one published watermark for the request.",
98
99
  inputSchema: queryWireSchema,
99
100
  annotations: READ_ONLY,
100
101
  }, (rawArgs) => {
@@ -109,7 +110,7 @@ export function registerLbbTools(server, client) {
109
110
  if (cursor && cursor.mode !== args.mode) {
110
111
  throw new Error(`cursor is for ${cursor.mode}, not ${args.mode}`);
111
112
  }
112
- assertCursorScope({ graph: args.graph, branch: args.branch }, cursor);
113
+ assertCursorScope({ graph: args.graph }, cursor);
113
114
  if (cursor &&
114
115
  args.row_limit !== undefined &&
115
116
  args.row_limit !== cursor.row_limit) {
@@ -118,9 +119,39 @@ export function registerLbbTools(server, client) {
118
119
  const detail = normalizeDetail(args.detail ?? cursor?.detail);
119
120
  const rowLimit = effectiveRowLimit(detail, args.row_limit ?? cursor?.row_limit);
120
121
  const graph = cursor?.graph ?? args.graph;
121
- const branch = cursor?.branch ?? args.branch;
122
122
  const offset = cursor?.offset ?? 0;
123
- const target = scoped(client, graph, branch);
123
+ const target = scoped(client, graph);
124
+ const timing = queryTiming(options, {
125
+ mode: args.mode,
126
+ continuation: cursor !== undefined,
127
+ row_limit: rowLimit,
128
+ offset,
129
+ });
130
+ const pin = (requested) => !cursor && requested === undefined
131
+ ? timing.measureAsync("query_pin_metadata", () => queryCommitPin(target, requested, cursor))
132
+ : queryCommitPin(target, requested, cursor);
133
+ const buildEnvelope = (value, rowPage, next, cursorBase, notes) => timing.measure("query_envelope", () => {
134
+ timing.counts({ received_rows: rowPage?.returned });
135
+ const result = queryEnvelope(`lbb_query.${args.mode}`, value, detail, rowPage, next, cursorBase, { textFormat: options.queryTextFormat, notes });
136
+ if (options.timing) {
137
+ const nextCursor = result.next?.cursor;
138
+ timing.counts({
139
+ returned_rows: rowPageFrom(result)?.returned,
140
+ cursor_bytes: typeof nextCursor === "string"
141
+ ? Buffer.byteLength(nextCursor, "utf8")
142
+ : 0,
143
+ });
144
+ }
145
+ return result;
146
+ });
147
+ const render = (value) => timing.measure("query_render", () => {
148
+ const result = queryToolResult(value, options.queryTextFormat);
149
+ if (options.timing)
150
+ timing.counts({
151
+ text_bytes: Buffer.byteLength(result.content[0].text, "utf8"),
152
+ });
153
+ return result;
154
+ });
124
155
  for (const key of [
125
156
  "entailment",
126
157
  "consistency",
@@ -174,7 +205,7 @@ export function registerLbbTools(server, client) {
174
205
  args.as_of_commit_seq !== cursor.as_of_commit_seq) {
175
206
  throw new Error("cursor as_of_commit_seq does not match the supplied as_of_commit_seq argument");
176
207
  }
177
- const asOfCommitSeq = await queryCommitPin(target, requestedCommitSeq, cursor);
208
+ const asOfCommitSeq = await pin(requestedCommitSeq);
178
209
  const request = {
179
210
  ...body,
180
211
  limit: rowLimit,
@@ -189,16 +220,15 @@ export function registerLbbTools(server, client) {
189
220
  request.combinators.length > 0) {
190
221
  throw new Error("`combinators` (UNION/OPTIONAL/MINUS/EXISTS) is no longer accepted by structured mode; the analytics route was removed. Express the same query as SPARQL text with mode=sparql.");
191
222
  }
192
- const response = await target.sparql(request, {
223
+ const response = await timing.measureAsync("query_http_total", () => target.sparql(request, {
193
224
  consistency,
194
225
  minIndexedSeq,
195
- });
226
+ }));
196
227
  const rowPage = rowPageFrom(response);
197
228
  const cursorBase = {
198
229
  v: 1,
199
230
  mode: "structured",
200
231
  graph,
201
- branch,
202
232
  detail,
203
233
  row_limit: rowLimit,
204
234
  body,
@@ -207,7 +237,7 @@ export function registerLbbTools(server, client) {
207
237
  as_of_commit_seq: asOfCommitSeq,
208
238
  };
209
239
  const next = rowPageNext(cursorBase, rowPage);
210
- return toolResult(queryEnvelope(`lbb_query.${args.mode}`, response, detail, rowPage, next, cursorBase));
240
+ return render(buildEnvelope(response, rowPage, next, cursorBase));
211
241
  }
212
242
  // Canonicalize little big brain relation/class/property IRI local-name case up
213
243
  // front, then use the normalized text everywhere (mismatch check,
@@ -229,22 +259,29 @@ export function registerLbbTools(server, client) {
229
259
  args.as_of_commit_seq !== cursor.as_of_commit_seq) {
230
260
  throw new Error("cursor as_of_commit_seq does not match the supplied as_of_commit_seq argument");
231
261
  }
232
- const asOfCommitSeq = await queryCommitPin(target, args.as_of_commit_seq, cursor);
262
+ const asOfCommitSeq = await pin(args.as_of_commit_seq);
233
263
  const entailment = cursor?.entailment ?? args.entailment ?? "none";
234
- const response = await target.sparqlText({
264
+ const response = await timing.measureAsync("query_http_total", () => target.sparqlText({
235
265
  query,
236
266
  entailment,
237
267
  as_of_commit_seq: asOfCommitSeq ?? null,
238
268
  limit: rowLimit,
239
269
  offset,
240
- }, { consistency, minIndexedSeq });
241
- const data = JSON.parse(response.results);
270
+ // Managed evals: the first page records a trace; a
271
+ // continuation page re-reads the same rows and must not.
272
+ ...(cursor === undefined && args.request
273
+ ? { request: args.request }
274
+ : {}),
275
+ }, { consistency, minIndexedSeq }));
276
+ if (response.trace_id) {
277
+ notes.push(`eval trace ${response.trace_id}: to label rows, read their item ids with lbb_evals action=trace trace_id=${response.trace_id}, then call lbb_evals action=label trace_id=${response.trace_id} item=<id> valid=true|false (or items=[…]).`);
278
+ }
279
+ const data = timing.measure("query_results_parse", () => JSON.parse(response.results));
242
280
  const rowPage = rowPageFrom(response);
243
281
  const cursorBase = {
244
282
  v: 1,
245
283
  mode: "sparql",
246
284
  graph,
247
- branch,
248
285
  detail,
249
286
  row_limit: rowLimit,
250
287
  query,
@@ -254,16 +291,32 @@ export function registerLbbTools(server, client) {
254
291
  as_of_commit_seq: asOfCommitSeq,
255
292
  };
256
293
  const next = rowPageNext(cursorBase, rowPage);
257
- const sparqlEnvelope = queryEnvelope(`lbb_query.${args.mode}`, data, detail, rowPage, next, cursorBase);
258
- return toolResult(notes.length > 0 ? { ...sparqlEnvelope, notes } : sparqlEnvelope);
294
+ const sparqlEnvelope = buildEnvelope(data, rowPage, next, cursorBase, notes);
295
+ return render(sparqlEnvelope);
259
296
  }
260
297
  catch (error) {
261
298
  return errorResult(await enrichError(client, error));
262
299
  }
263
300
  })();
264
301
  }
302
+ if (args.mode === "search") {
303
+ const searchArgs = args;
304
+ return run(client, "lbb_query.search", undefined, async () => {
305
+ const target = scoped(client, searchArgs.graph);
306
+ return target.embeddings.search({
307
+ embedding: searchArgs.embedding,
308
+ text: searchArgs.text,
309
+ top_k: searchArgs.top_k,
310
+ probe: searchArgs.probe,
311
+ include: searchArgs.include,
312
+ request: searchArgs.request,
313
+ filter: searchArgs.filter,
314
+ explain: searchArgs.explain,
315
+ });
316
+ });
317
+ }
265
318
  return run(client, `lbb_query.${args.mode}`, args.detail, async () => {
266
- const target = scoped(client, args.graph, args.branch);
319
+ const target = scoped(client, args.graph);
267
320
  return analyze(target, {
268
321
  metric: args.metric,
269
322
  chart: args.chart,
@@ -277,13 +330,7 @@ export function registerLbbTools(server, client) {
277
330
  server.registerTool("lbb_models", {
278
331
  description: "Read model-training inputs or compare retrieval configurations over one pinned published snapshot. shadow_eval takes the API ShadowEvalRequest body; dataset actions return bounded training examples at an optional signal split.",
279
332
  inputSchema: {
280
- action: z.enum([
281
- "shadow_eval",
282
- "planner_dataset",
283
- "planner_preference_dataset",
284
- "suggest_dataset",
285
- "extractor_dataset",
286
- ]),
333
+ action: z.enum(["shadow_eval", "suggest_dataset", "extractor_dataset"]),
287
334
  body: jsonObjectSchema.optional(),
288
335
  limit: z.number().int().positive().optional(),
289
336
  split_seq: z.number().int().nonnegative().optional(),
@@ -291,26 +338,270 @@ export function registerLbbTools(server, client) {
291
338
  ...graphScope,
292
339
  },
293
340
  annotations: READ_ONLY,
294
- }, ({ action, body, limit, split_seq, detail, graph, branch }) => run(client, `lbb_models.${action}`, detail, () => {
295
- const target = scoped(client, graph, branch);
341
+ }, ({ action, body, limit, split_seq, detail, graph }) => run(client, `lbb_models.${action}`, detail, () => {
342
+ const target = scoped(client, graph);
296
343
  switch (action) {
297
344
  case "shadow_eval":
298
345
  if (!body)
299
346
  throw new Error("shadow_eval requires body");
300
347
  return target.shadowEval(body);
301
- case "planner_dataset":
302
- return target.plannerDataset({ limit, splitSeq: split_seq });
303
- case "planner_preference_dataset":
304
- return target.plannerPreferenceDataset({
305
- limit,
306
- splitSeq: split_seq,
307
- });
308
348
  case "suggest_dataset":
309
349
  return target.suggestDataset({ limit, splitSeq: split_seq });
310
350
  case "extractor_dataset":
311
351
  return target.extractorDataset({ limit, splitSeq: split_seq });
312
352
  }
313
353
  }));
354
+ const embeddingSetup = {
355
+ class: z.string().optional().describe("preview/declare: the class IRI."),
356
+ from: z
357
+ .array(z.string())
358
+ .optional()
359
+ .describe('preview/declare: the fields, e.g. ["label", "description", "calls/label"]. Omit for the automatic choice; on an existing embedding, omit to keep its fields.'),
360
+ exclude: z
361
+ .array(z.string())
362
+ .optional()
363
+ .describe("preview/declare: fields to drop from the automatic choice."),
364
+ title: z
365
+ .string()
366
+ .optional()
367
+ .describe('preview/declare: the field that names each hit, e.g. "display_name". Omit for the ontology\'s name property (skos:prefLabel, a property declared rdfs:subPropertyOf rdfs:label, else rdfs:label); on an existing embedding, omit to keep its name. preview reports title_source and, when the labels read as keys, title_suggestion.'),
368
+ model: z
369
+ .string()
370
+ .optional()
371
+ .describe("model: the graph's new model. preview/declare: omit it; a graph has one model (the graph's, or the platform's default for the first embedding)."),
372
+ dim: z.number().int().positive().optional(),
373
+ };
374
+ const recipeOf = (args) => {
375
+ if (!args.class)
376
+ throw new Error("this action requires class");
377
+ return {
378
+ class: args.class,
379
+ ...(args.name ? { name: args.name } : {}),
380
+ ...(args.from ? { from: args.from } : {}),
381
+ ...(args.exclude ? { exclude: args.exclude } : {}),
382
+ ...(args.title ? { title: args.title } : {}),
383
+ ...(args.model ? { model: args.model } : {}),
384
+ ...(args.dim ? { dim: args.dim } : {}),
385
+ };
386
+ };
387
+ server.registerTool("lbb_embeddings", {
388
+ description: "Search setup, read only: embeddings declared on classes (an embedding on a class covers its subclasses; one model per graph). list shows each embedding's status (serving and building version, backfill progress, lag) and the graph's model; get shows one; preview shows every candidate fact of a class with its coverage and examples, and the exact text of sample instances, and stores nothing. Declare, refresh, or change the model with lbb_embeddings_manage; search with lbb_query mode=search. list is the first step of planning a search (lbb_inspect action=guide has the recipe).",
389
+ inputSchema: {
390
+ action: z.enum(["list", "get", "preview"]),
391
+ name: z.string().optional().describe("get: the embedding name."),
392
+ ...embeddingSetup,
393
+ sample: z.number().int().positive().max(50).optional(),
394
+ iris: z
395
+ .array(z.string())
396
+ .optional()
397
+ .describe("preview: the instances to show instead of a sample."),
398
+ detail: detailSchema,
399
+ ...graphScope,
400
+ },
401
+ annotations: READ_ONLY,
402
+ }, (args) => run(client, `lbb_embeddings.${args.action}`, args.detail, () => {
403
+ const target = scoped(client, args.graph);
404
+ switch (args.action) {
405
+ case "list":
406
+ return target.embeddings.list();
407
+ case "get":
408
+ if (!args.name)
409
+ throw new Error("get requires name");
410
+ return target.embeddings.get(args.name);
411
+ case "preview":
412
+ return target.embeddings.preview({
413
+ ...recipeOf(args),
414
+ ...(args.sample ? { sample: args.sample } : {}),
415
+ ...(args.iris ? { iris: args.iris } : {}),
416
+ });
417
+ }
418
+ }));
419
+ server.registerTool("lbb_embeddings_manage", {
420
+ description: "Declare or refresh an embedding, or change the graph's model. Run lbb_embeddings action=preview first and show the user the text it will embed. declare sets the class, the fields (one-hop property paths such as label, description, calls/label; omit for an automatic choice); a new embedding takes the graph's model, and another model is refused. On an existing embedding it changes only what you name, and a changed recipe builds a new version while the old one serves. model moves every embedding of the graph to `model`: each builds a new version, and the graph switches when all are ready (a search never mixes two models). refresh runs one step of the embed job now (it also runs by itself after each published commit).",
421
+ inputSchema: {
422
+ action: z.enum(["declare", "refresh", "model"]),
423
+ name: z
424
+ .string()
425
+ .optional()
426
+ .describe("The embedding name (refresh: required)."),
427
+ ...embeddingSetup,
428
+ detail: detailSchema,
429
+ ...graphScope,
430
+ },
431
+ annotations: MUTATING,
432
+ }, (args) => run(client, `lbb_embeddings_manage.${args.action}`, args.detail, () => {
433
+ const target = scoped(client, args.graph);
434
+ switch (args.action) {
435
+ case "declare":
436
+ return target.embeddings.declare(recipeOf(args));
437
+ case "refresh":
438
+ if (!args.name)
439
+ throw new Error("refresh requires name");
440
+ return target.embeddings.refresh(args.name);
441
+ case "model":
442
+ if (!args.model)
443
+ throw new Error("model requires model");
444
+ return target.embeddings.setModel({
445
+ model: args.model,
446
+ ...(args.dim ? { dim: args.dim } : {}),
447
+ });
448
+ }
449
+ }));
450
+ server.registerTool("lbb_embeddings_delete", {
451
+ description: "Delete an embedding: its name and its vectors (index-gc removes the stored runs). Its class stops being searchable until it is declared and built again. Ask the user first; confirm must repeat the name.",
452
+ inputSchema: {
453
+ name: z.string().describe("The embedding name."),
454
+ confirm: z.string().describe("The same name again, to confirm."),
455
+ detail: detailSchema,
456
+ ...graphScope,
457
+ },
458
+ annotations: DESTRUCTIVE,
459
+ }, ({ name, confirm, detail, graph }) => run(client, "lbb_embeddings_delete", detail, () => {
460
+ if (confirm !== name) {
461
+ throw new Error("confirm must repeat the embedding name");
462
+ }
463
+ return scoped(client, graph).embeddings.delete(name);
464
+ }));
465
+ server.registerTool("lbb_evals", {
466
+ description: "Managed evals: the thumbs up / thumbs down of the graph, per result. A query run with `request` (lbb_query) records a trace with one item per result (hit or row); trace reads it back with the item ids. label marks one result (item + valid) or several (items) relevant or not; the labels become the golden's ground truth. golden freezes a query (every result it returns now is relevant). run replays every golden at the current commit: pass when every relevant result is back and no wrong one is; new results open a review trace. judge lets the platform's judge model (the hosted frontier model) label unlabeled results. summary, traces, goldens, results, and settings read state.",
467
+ inputSchema: {
468
+ action: z.enum([
469
+ "summary",
470
+ "traces",
471
+ "trace",
472
+ "label",
473
+ "judge",
474
+ "goldens",
475
+ "golden",
476
+ "accept",
477
+ "delete",
478
+ "run",
479
+ "results",
480
+ "settings",
481
+ ]),
482
+ trace_id: z
483
+ .string()
484
+ .optional()
485
+ .describe("trace / label / judge: the trace id from lbb_query."),
486
+ item: z
487
+ .string()
488
+ .optional()
489
+ .describe("label: the result id (a hit's `id`, or an item id from trace) to label with `valid`."),
490
+ valid: z
491
+ .boolean()
492
+ .optional()
493
+ .describe("label: true = the result answers the request (thumbs up), false = it does not."),
494
+ items: z
495
+ .array(z
496
+ .object({
497
+ id: z.string(),
498
+ valid: z.boolean(),
499
+ note: z.string().optional(),
500
+ })
501
+ .strict())
502
+ .optional()
503
+ .describe("label: several results at once."),
504
+ by: z
505
+ .string()
506
+ .optional()
507
+ .describe("label: who labels (an agent name)."),
508
+ note: z.string().optional(),
509
+ sparql: z
510
+ .string()
511
+ .optional()
512
+ .describe("golden: the query to freeze (the search text for surface=search)."),
513
+ surface: z
514
+ .enum(["sparql", "search"])
515
+ .optional()
516
+ .describe("golden: which surface the query runs on (default sparql)."),
517
+ embedding: z
518
+ .string()
519
+ .optional()
520
+ .describe("golden: the embedding, for surface=search."),
521
+ top_k: z
522
+ .number()
523
+ .int()
524
+ .positive()
525
+ .max(200)
526
+ .optional()
527
+ .describe("golden: the top_k of a search golden."),
528
+ request: z
529
+ .string()
530
+ .optional()
531
+ .describe("golden: the user's words the query answers."),
532
+ golden_id: z
533
+ .string()
534
+ .optional()
535
+ .describe("accept / delete: the golden id."),
536
+ limit: z.number().int().positive().optional(),
537
+ unlabeled: z
538
+ .boolean()
539
+ .optional()
540
+ .describe("traces: only traces without a label."),
541
+ consistency: z
542
+ .enum(["strong", "eventual"])
543
+ .optional()
544
+ .describe("run / accept: strong reads the head, eventual the last published commit."),
545
+ detail: detailSchema,
546
+ ...graphScope,
547
+ },
548
+ annotations: MUTATING,
549
+ }, ({ action, trace_id, item, valid, items, by, note, sparql, surface, embedding, top_k, request, golden_id, limit, unlabeled, consistency, detail, graph, }) => run(client, `lbb_evals.${action}`, detail, () => {
550
+ const target = scoped(client, graph);
551
+ switch (action) {
552
+ case "summary":
553
+ return target.evals.summary();
554
+ case "traces":
555
+ return target.evals.traces({ limit, unlabeled });
556
+ case "trace":
557
+ if (!trace_id)
558
+ throw new Error("trace requires trace_id");
559
+ return target.evals.trace(trace_id);
560
+ case "label":
561
+ if (!trace_id)
562
+ throw new Error("label requires trace_id");
563
+ if (!item && !items?.length)
564
+ throw new Error("label requires item (with valid) or items");
565
+ if (item && valid === undefined)
566
+ throw new Error("label requires valid with item");
567
+ return target.evals.label(trace_id, {
568
+ ...(item ? { item, valid } : {}),
569
+ ...(items?.length ? { items } : {}),
570
+ by,
571
+ note,
572
+ });
573
+ case "judge":
574
+ return target.evals.judge({ traceId: trace_id, limit });
575
+ case "goldens":
576
+ return target.evals.goldens();
577
+ case "golden":
578
+ if (!sparql)
579
+ throw new Error("golden requires sparql");
580
+ if (surface === "search" && !embedding)
581
+ throw new Error("a search golden requires embedding");
582
+ return target.evals.createGolden({
583
+ sparql,
584
+ request,
585
+ ...(surface ? { surface } : {}),
586
+ ...(embedding ? { embedding } : {}),
587
+ ...(top_k ? { top_k } : {}),
588
+ });
589
+ case "accept":
590
+ if (!golden_id)
591
+ throw new Error("accept requires golden_id");
592
+ return target.evals.acceptGolden(golden_id, { consistency });
593
+ case "delete":
594
+ if (!golden_id)
595
+ throw new Error("delete requires golden_id");
596
+ return target.evals.deleteGolden(golden_id);
597
+ case "run":
598
+ return target.evals.run({ consistency });
599
+ case "results":
600
+ return target.evals.results({ limit });
601
+ case "settings":
602
+ return target.evals.settings();
603
+ }
604
+ }));
314
605
  server.registerTool("lbb_commit", {
315
606
  description: "Write graph facts, retract them, or label ranked results. mode=facts writes triplets/embeddings/properties; mode=retract removes a wrongly-added fact (by edge or by entity) without a full reset; mode=search_feedback stores query/result relevance labels (Feedback grades: 3=ideal/good, 1=partial, 0=bad; include query, search_id when available, target, rank, score). Explicit idempotency_key wins; when omitted, MCP derives a stable content hash so content-identical retries dedupe. Facts mode defaults edge_idempotency to append; pass skip_unchanged for re-runnable backfills.",
316
607
  inputSchema: {
@@ -377,7 +668,7 @@ export function registerLbbTools(server, client) {
377
668
  ...graphScope,
378
669
  },
379
670
  annotations: IDEMPOTENT_WRITE,
380
- }, ({ idempotency_key, mode, triplets, entity_embeddings, entity_properties, search_feedback, dry_run, observed_at, edge_idempotency, retract_edges, retract_entities, graph, branch, }) => run(client, "lbb_commit", "standard", () => {
671
+ }, ({ idempotency_key, mode, triplets, entity_embeddings, entity_properties, search_feedback, dry_run, observed_at, edge_idempotency, retract_edges, retract_entities, graph, }) => run(client, "lbb_commit", "standard", () => {
381
672
  const commitMode = mode ??
382
673
  (search_feedback
383
674
  ? "search_feedback"
@@ -393,8 +684,8 @@ export function registerLbbTools(server, client) {
393
684
  throw new Error("lbb_commit mode=retract requires retract_edges or retract_entities");
394
685
  }
395
686
  const key = idempotency_key ??
396
- contentHashKey({ graph, branch }, { mode: "retract", edges, entities });
397
- return scoped(client, graph, branch).retract({ edges, entities }, {
687
+ contentHashKey({ graph }, { mode: "retract", edges, entities });
688
+ return scoped(client, graph).retract({ edges, entities }, {
398
689
  idempotencyKey: key,
399
690
  });
400
691
  }
@@ -402,8 +693,8 @@ export function registerLbbTools(server, client) {
402
693
  if (!search_feedback)
403
694
  throw new Error("lbb_commit mode=search_feedback requires search_feedback");
404
695
  const key = idempotency_key ??
405
- contentHashKey({ graph, branch }, { mode: "search_feedback", search_feedback });
406
- return scoped(client, graph, branch).searchFeedback(search_feedback, { idempotencyKey: key });
696
+ contentHashKey({ graph }, { mode: "search_feedback", search_feedback });
697
+ return scoped(client, graph).searchFeedback(search_feedback, { idempotencyKey: key });
407
698
  }
408
699
  if (search_feedback) {
409
700
  throw new Error("lbb_commit facts mode cannot include search_feedback");
@@ -420,10 +711,10 @@ export function registerLbbTools(server, client) {
420
711
  payload.entity_properties.length === 0) {
421
712
  throw new Error("lbb_commit requires at least one triplet, entity embedding, or entity property");
422
713
  }
423
- const key = idempotency_key ?? contentHashKey({ graph, branch }, payload);
714
+ const key = idempotency_key ?? contentHashKey({ graph }, payload);
424
715
  if (dry_run)
425
- return scoped(client, graph, branch).commitDryRun(payload);
426
- return scoped(client, graph, branch).commit(payload, {
716
+ return scoped(client, graph).commitDryRun(payload);
717
+ return scoped(client, graph).commit(payload, {
427
718
  idempotencyKey: key,
428
719
  });
429
720
  }));
@@ -438,9 +729,7 @@ export function registerLbbTools(server, client) {
438
729
  const args = parsed.data;
439
730
  return run(client, `lbb_configure.${args.action}`, "standard", () => {
440
731
  if (args.action === "define_ontology") {
441
- return client
442
- .withScope({ graph: args.graph, branch: args.branch })
443
- .ontologyDefine(ontologyDefineBody({
732
+ return client.withScope({ graph: args.graph }).ontologyDefine(ontologyDefineBody({
444
733
  entity_types: args.entity_types,
445
734
  relations: args.relations,
446
735
  source: args.source,
@@ -450,7 +739,7 @@ export function registerLbbTools(server, client) {
450
739
  }));
451
740
  }
452
741
  if (args.action === "evolve_ontology") {
453
- return scoped(client, args.graph, args.branch).ontology.evolve({
742
+ return scoped(client, args.graph).ontology.evolve({
454
743
  ops: args.ops,
455
744
  allow_data_conflicts: args.allow_data_conflicts ?? false,
456
745
  }, { dryRun: args.dry_run });
@@ -458,7 +747,7 @@ export function registerLbbTools(server, client) {
458
747
  if (args.shapes === undefined) {
459
748
  throw new Error("publish_schema requires a SHACL shapes source; use define_ontology or evolve_ontology for native metadata changes");
460
749
  }
461
- return scoped(client, args.graph, args.branch).schema.publish({
750
+ return scoped(client, args.graph).schema.publish({
462
751
  ontology: args.ontology,
463
752
  shapes: args.shapes,
464
753
  desired_mode: args.desired_mode,
@@ -466,85 +755,4 @@ export function registerLbbTools(server, client) {
466
755
  }, { dryRun: args.dry_run });
467
756
  });
468
757
  });
469
- server.registerTool("lbb_branch", {
470
- description: "Branch lifecycle. Actions: create (fork a new branch off from_branch — the tool's `branch` argument names the NEW branch) and merge (validate-then-merge: replay from_branch's post-fork commits onto the scoped target branch — its fork parent — as ONE commit with event ids preserved; SHACL-validates the would-be merged state first and refuses with the report on violations; a fact superseded on the target after the fork wins over the branch's version, reported as a supersedure_race conflict; delete_source consumes the merged branch).",
471
- inputSchema: {
472
- action: z
473
- .enum(["create", "merge"])
474
- .describe("create = fork a new branch; merge = replay a child branch onto its fork parent"),
475
- from_branch: z
476
- .string()
477
- .describe("create: the branch to fork from; merge: the child branch whose commits are replayed"),
478
- validate: z
479
- .boolean()
480
- .optional()
481
- .describe("merge only: refuse on SHACL violations of the would-be merged state (default true)"),
482
- delete_source: z
483
- .boolean()
484
- .optional()
485
- .describe("merge only: delete every object under the merged branch after success"),
486
- ...graphScope,
487
- },
488
- annotations: MUTATING,
489
- }, ({ action, from_branch, validate, delete_source, graph, branch }) => run(client, `lbb_branch.${action}`, "standard", () => {
490
- const target = scoped(client, graph, branch);
491
- if (action === "create")
492
- return target.createBranch({ from_branch });
493
- return target.mergeBranch({
494
- from_branch,
495
- validate: validate ?? true,
496
- delete_source: delete_source ?? false,
497
- });
498
- }));
499
- server.registerTool("lbb_observe", {
500
- description: "Remember a conversation: store the turns verbatim as an EPISODE evidence entity, then anchor + gate the supplied facts on an observe branch (LLM extraction cannot poison the main graph). Facts with both endpoints already in the graph are anchored; unanchored facts need confidence >= 0.8 to mint new entities, else they come back needs_review. auto_merge merges the branch onto the scoped branch when SHACL validation is clean (the validate-then-merge). Server flag-gated (--enable-observe). This build takes caller-extracted facts (each with a structured triplet); bare statements come back needs_review.",
501
- inputSchema: {
502
- session_id: z
503
- .string()
504
- .describe("Caller's conversation id (drives the default observe branch name)"),
505
- turns: z
506
- .array(z.object({
507
- role: z.string().describe("user | assistant | tool"),
508
- content: z.string(),
509
- name: z.string().optional(),
510
- ts: z.string().optional().describe("RFC 3339 timestamp"),
511
- }))
512
- .min(1)
513
- .describe("The conversation slice to remember (stored verbatim)"),
514
- source: z
515
- .string()
516
- .optional()
517
- .describe("Source label, e.g. support-bot"),
518
- facts: z
519
- .array(z.object({
520
- fact: z.string().describe("Natural-language statement"),
521
- confidence: z.number().optional().describe("0..1 (default 0.9)"),
522
- triplet: jsonObjectSchema
523
- .optional()
524
- .describe("Structured form {source:{type,name}, relation, target:{type,name}} — required for the fact to commit"),
525
- }))
526
- .optional()
527
- .describe("Caller-extracted candidate facts; omit with extract:false to store the episode only"),
528
- extract: z
529
- .boolean()
530
- .optional()
531
- .describe("false = store the episode only (default true)"),
532
- observe_branch: z
533
- .string()
534
- .optional()
535
- .describe("Branch for the facts (default observe-<hash12(session_id)>)"),
536
- auto_merge: z
537
- .boolean()
538
- .optional()
539
- .describe("Merge onto the scoped branch when validation is clean"),
540
- ...graphScope,
541
- },
542
- annotations: MUTATING,
543
- }, ({ session_id, turns, source, facts, extract, observe_branch, auto_merge, graph, branch, }) => run(client, "lbb_observe", "standard", () => scoped(client, graph, branch).observe({
544
- episode: { turns, session_id, source },
545
- extract: extract ?? true,
546
- extraction: { byo_completion: (facts ?? []) },
547
- branch: observe_branch,
548
- auto_merge: auto_merge ?? false,
549
- })));
550
758
  }