@littlebigbrain/mcp 0.10.0 → 0.12.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/question.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { lexicalRow, listLimit } from "./query-tools.js";
1
2
  import { QUESTION_MAX_ROWS } from "./tool-contracts.js";
2
3
  import { defaultRowLimit, normalizeDetail, queryEnvelope, queryToolResult, rowPageFrom, rowPageNext, scoped, } from "./tool-runtime.js";
3
- const REPAIR_ERROR_CHARS = 200;
4
4
  /** The progress message of a stream event, in short plain words. */
5
5
  export function progressMessage(event) {
6
6
  const data = (event.data ?? {});
@@ -13,41 +13,35 @@ export function progressMessage(event) {
13
13
  : "";
14
14
  return `Route: ${String(data.kind)} (${String(data.by)}${confidence})`;
15
15
  }
16
- case "query":
17
- return `Wrote query ${String(data.attempt ?? 1)}`;
18
- case "run":
19
- return data.point === "before"
20
- ? "Running the query at the earlier point"
21
- : data.point === "after"
22
- ? "Running the query at the later point"
23
- : "Running the query";
24
- case "rows": {
25
- const count = Number(data.count);
26
- return `${count} ${count === 1 ? "row" : "rows"} in ${String(data.ms)} ms`;
27
- }
28
- case "repair": {
29
- const error = String(data.error ?? "");
30
- const shown = error.length > REPAIR_ERROR_CHARS
31
- ? `${error.slice(0, REPAIR_ERROR_CHARS - 1)}…`
32
- : error;
33
- return `Correcting the query: ${shown}`;
16
+ case "step": {
17
+ // A compare step counts the entries that differ, not rows.
18
+ const unit = data.tool === "compare" ? "difference" : "row";
19
+ const rows = typeof data.rows === "number"
20
+ ? `: ${data.rows} ${unit}${data.rows === 1 ? "" : "s"}`
21
+ : "";
22
+ const outcome = data.ok === false ? ": failed" : rows;
23
+ return `Step ${String(data.n)}, ${String(data.tool)} ${String(data.input ?? "")}${outcome}`;
34
24
  }
25
+ case "answer":
26
+ return "Answered";
35
27
  default:
36
28
  return undefined;
37
29
  }
38
30
  }
39
31
  /**
40
- * The rewrite as a stream of progress events. Each event becomes one
32
+ * The question as a stream of progress events. Each event becomes one
41
33
  * progress notification; the `done` event is the same response as the call
42
- * without a stream. A client release without `query.rewriteStream` answers
34
+ * without a stream. A client release without `query.askStream` answers
43
35
  * without progress.
44
36
  */
45
- async function streamedRewrite(target, body, progress) {
37
+ async function streamedAnswer(target, body, progress) {
46
38
  const query = target.query;
47
- if (typeof query.rewriteStream !== "function")
39
+ if (typeof query.askStream !== "function")
48
40
  return undefined;
41
+ const { question, ...options } = body;
49
42
  let step = 0;
50
- for await (const event of query.rewriteStream.call(target.query, body, {
43
+ for await (const event of query.askStream.call(target.query, question, {
44
+ ...options,
51
45
  signal: progress.signal,
52
46
  })) {
53
47
  if (event.event === "done")
@@ -59,30 +53,50 @@ async function streamedRewrite(target, body, progress) {
59
53
  // A lost notification never fails the question.
60
54
  await progress.report(step, message).catch(() => undefined);
61
55
  }
62
- throw new Error("the rewrite stream ended before its done event");
56
+ throw new Error("the answer stream ended before its done event");
63
57
  }
64
- /** A row of a comparison as `{ variable: lexical value }`. */
65
- function lexicalRow(row) {
66
- return Object.fromEntries(Object.entries(row).map(([name, term]) => [name, term.value]));
58
+ /** The totals of a comparison: the server's, else the lengths of its lists. */
59
+ function historyTotals(history) {
60
+ return (history.totals ?? {
61
+ added: history.added?.length ?? 0,
62
+ removed: history.removed?.length ?? 0,
63
+ changed: history.changed?.length ?? 0,
64
+ });
67
65
  }
68
66
  /**
69
67
  * The history the tool returns: where the question read the graph, and for
70
- * a comparison the rows that differ as lexical values. The two runs stay
71
- * out: the later one is the result, and the rows that differ say what the
72
- * earlier one held.
68
+ * a comparison the rows that differ as lexical values, with the total of
69
+ * each list. Each list shows at most `limit` entries; `shown` says how many
70
+ * when a list is cut.
73
71
  */
74
- function historyView(history) {
72
+ function historyView(history, limit) {
75
73
  const view = { ...history };
76
- delete view.before;
77
- delete view.after;
78
- if (history.added)
79
- view.added = history.added.map(lexicalRow);
80
- if (history.removed)
81
- view.removed = history.removed.map(lexicalRow);
74
+ if (!history.added && !history.removed && !history.changed)
75
+ return view;
76
+ const totals = historyTotals(history);
77
+ view.totals = totals;
78
+ const shown = {};
79
+ const show = (name, list, map) => {
80
+ if (!list)
81
+ return;
82
+ const kept = list.slice(0, limit);
83
+ view[name] = kept.map(map);
84
+ if (kept.length < (totals[name] ?? list.length))
85
+ shown[name] = kept.length;
86
+ };
87
+ show("added", history.added, lexicalRow);
88
+ show("removed", history.removed, lexicalRow);
89
+ show("changed", history.changed, (change) => ({
90
+ key: lexicalRow(change.key),
91
+ before: change.before.map(lexicalRow),
92
+ after: change.after.map(lexicalRow),
93
+ }));
94
+ if (Object.keys(shown).length)
95
+ view.shown = shown;
82
96
  return view;
83
97
  }
84
98
  /** What the agent should know about the history of the answer. */
85
- function historyNotes(history) {
99
+ function historyNotes(history, limit, query) {
86
100
  if (!history)
87
101
  return [];
88
102
  const notes = [];
@@ -100,36 +114,55 @@ function historyNotes(history) {
100
114
  else if (history.as_of_date) {
101
115
  notes.push(`The server could not place ${history.as_of_date} (see error). Pass timeline=[{date, as_of_commit_seq}] to name the commit of each date, or run the query with lbb_query mode=sparql as_of_commit_seq=<commit>.`);
102
116
  }
103
- if (history.compare && history.added && point) {
104
- notes.push(`A comparison: the rows are the later run; history.added and history.removed hold the rows that changed since ${point}.${history.truncated ? " The difference is not complete: a run returned only its first rows, or more than 500 rows changed." : ""}`);
117
+ if (history.compare && (history.added || history.changed) && point) {
118
+ const totals = historyTotals(history);
119
+ // The compared query reads the later point.
120
+ const later = typeof query?.as_of_commit_seq === "number"
121
+ ? query.as_of_commit_seq
122
+ : undefined;
123
+ const keyed = history.key?.length
124
+ ? `paired by ${history.key.map((v) => `?${v}`).join(", ")}: history.added and history.removed hold the entities at one point only, history.changed the entities whose values differ`
125
+ : "history.added and history.removed hold the rows that differ (a changed value is a removed row and an added row)";
126
+ notes.push(`A comparison from ${point} to ${later === undefined ? "the latest commit" : `commit ${later}`}; ${keyed}. Totals: added ${totals.added}, removed ${totals.removed}, changed ${totals.changed}.`);
127
+ const cut = ["added", "removed", "changed"].filter((name) => totals[name] > limit);
128
+ if (cut.length && query) {
129
+ const after = later === undefined ? "" : ` after={"as_of_commit_seq": ${later}}`;
130
+ const key = history.key?.length
131
+ ? ` key=${JSON.stringify(history.key)}`
132
+ : "";
133
+ notes.push(`Each list shows at most ${limit} entries (${cut.map((name) => `${name}: ${totals[name]}`).join(", ")}; see history.shown). Read them all, page by page, with lbb_query mode=compare query=<the query> before={"as_of_commit_seq": ${history.as_of_commit_seq}}${after}${key}.`);
134
+ }
135
+ if (history.truncated) {
136
+ notes.push("The difference is not complete: a point was not read whole, or a list held more than 500 entries on the server. Use lbb_query mode=compare for the whole difference.");
137
+ }
105
138
  }
106
139
  return notes;
107
140
  }
108
141
  /**
109
- * `lbb_query mode=question`: the server turns the question into a SPARQL
110
- * query and, by default, runs it. The rows are bounded as `mode=sparql`
111
- * bounds them, and `next` continues the same query with `mode=sparql`.
112
- * With `progress`, the server streams its steps and each step is reported;
113
- * the result is the same.
142
+ * `lbb_query mode=question`: the server answers the question in plain words
143
+ * (`POST /v1/query/ask`). Its model runs queries in a bounded loop and
144
+ * reads their rows. The result holds the answer, its citations and chart
145
+ * hint, the steps, and the rows of the query the answer stands on, bounded
146
+ * as `mode=sparql`
147
+ * bounds them; `next` continues that query with `mode=sparql`. With
148
+ * `progress`, the server streams its steps and each step is reported; the
149
+ * result is the same.
114
150
  */
115
151
  export async function answerQuestion(client, args, textFormat, progress) {
116
152
  const detail = normalizeDetail(args.detail);
117
- const run = args.run ?? true;
118
153
  const rowLimit = Math.min(args.limit ?? defaultRowLimit(detail), QUESTION_MAX_ROWS);
119
154
  const target = scoped(client, args.graph);
120
155
  const body = {
121
156
  question: args.question,
122
157
  ...(args.context !== undefined ? { context: args.context } : {}),
123
158
  ...(args.route !== undefined ? { route: args.route } : {}),
124
- run,
125
- ...(run ? { limit: rowLimit } : {}),
159
+ limit: rowLimit,
126
160
  ...(args.anchor?.length ? { anchor: args.anchor } : {}),
127
161
  ...(args.timeline?.length ? { timeline: args.timeline } : {}),
128
162
  };
129
- // Each call uses model tokens, and the server corrects a failing query
130
- // once, so a failed call is not retried.
131
- const response = (progress ? await streamedRewrite(target, body, progress) : undefined) ??
132
- (await target.request("POST", "/v1/query/rewrite", {
163
+ // Each call uses model tokens, so a failed call is not retried.
164
+ const response = (progress ? await streamedAnswer(target, body, progress) : undefined) ??
165
+ (await target.request("POST", "/v1/query/ask", {
133
166
  body,
134
167
  retry: false,
135
168
  query: { consistency: target.defaultConsistency },
@@ -137,6 +170,20 @@ export async function answerQuestion(client, args, textFormat, progress) {
137
170
  const { route, query, result } = response;
138
171
  const traceId = result?.trace_id ?? null;
139
172
  const answer = {
173
+ answer: response.answer
174
+ ? {
175
+ text: response.answer.text,
176
+ citations: response.answer.citations ?? [],
177
+ ...(response.answer.chart ? { chart: response.answer.chart } : {}),
178
+ }
179
+ : null,
180
+ steps: (response.steps ?? []).map((step) => ({
181
+ n: step.n,
182
+ tool: step.tool,
183
+ ok: step.ok,
184
+ ...(typeof step.rows === "number" ? { rows: step.rows } : {}),
185
+ ...(step.error ? { error: step.error } : {}),
186
+ })),
140
187
  route: { kind: route.kind, confidence: route.confidence, by: route.by },
141
188
  rationale: response.rationale,
142
189
  sparql: query?.sparql ?? null,
@@ -144,7 +191,9 @@ export async function answerQuestion(client, args, textFormat, progress) {
144
191
  ...(typeof query?.as_of_commit_seq === "number"
145
192
  ? { as_of_commit_seq: query.as_of_commit_seq }
146
193
  : {}),
147
- ...(response.history ? { history: historyView(response.history) } : {}),
194
+ ...(response.history
195
+ ? { history: historyView(response.history, listLimit(detail)) }
196
+ : {}),
148
197
  error: response.error ?? null,
149
198
  trace_id: traceId,
150
199
  attempts: response.attempts,
@@ -160,9 +209,9 @@ export async function answerQuestion(client, args, textFormat, progress) {
160
209
  if (traceId) {
161
210
  notes.push(`eval trace ${traceId}: to label rows, read their item ids with lbb_evals action=trace trace_id=${traceId}, then call lbb_evals action=label trace_id=${traceId} item=<id> valid=true|false (or items=[…]).`);
162
211
  }
163
- notes.push(...historyNotes(response.history));
164
- if (response.error && query) {
165
- notes.push("The last query failed (see error). Correct it and run it with lbb_query mode=sparql, or ask again with more context.");
212
+ notes.push(...historyNotes(response.history, listLimit(detail), query));
213
+ if (!response.answer) {
214
+ notes.push("The answer loop stopped before it answered (see error); the rows are the best ones it read. Continue with lbb_query mode=sparql, or ask again with more context.");
166
215
  }
167
216
  const label = `lbb_query.question: route ${route.kind} (${route.by}, confidence ${route.confidence.toFixed(2)})`;
168
217
  const data = result
@@ -186,13 +235,15 @@ export async function answerQuestion(client, args, textFormat, progress) {
186
235
  };
187
236
  return queryToolResult(queryEnvelope(label, { ...answer, ...data }, detail, rowPage, rowPageNext(cursorBase, rowPage), cursorBase, { textFormat, notes }), textFormat);
188
237
  }
189
- // No rows to page: the query only, an ASK answer, or no query at all. The
238
+ // No rows to page: an ASK answer, a comparison, or no query at all. The
190
239
  // query text is returned whole.
191
240
  const summary = data
192
241
  ? `${label}: ran the query`
193
- : query
194
- ? `${label}: ${response.error ? "the query failed" : "the query, not run"}`
195
- : `${label}: no query`;
242
+ : response.history?.compare
243
+ ? `${label}: compared two points`
244
+ : query
245
+ ? `${label}: no rows`
246
+ : `${label}: no query`;
196
247
  return queryToolResult({
197
248
  summary,
198
249
  data: { ...answer, ...(data ?? {}) },
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  export const DEFAULT_DETAIL = "compact";
3
3
  export const HARD_OUTPUT_CHARS = 80_000;
4
4
  export const MAX_QUERY_ROW_LIMIT = 5_000;
5
- /** The most rows `POST /v1/query/rewrite` returns from a run. */
5
+ /** The most rows each query of `POST /v1/query/ask` returns. */
6
6
  export const QUESTION_MAX_ROWS = 1_000;
7
7
  /** Entity IRIs one question may anchor, and the characters of one. */
8
8
  export const QUESTION_MAX_ANCHORS = 10;
@@ -331,6 +331,73 @@ export const ontologyEvolveOpSchema = z.discriminatedUnion("op", [
331
331
  })
332
332
  .strict(),
333
333
  ]);
334
+ const namedEntityInputSchema = z
335
+ .object({
336
+ type: z.string(),
337
+ name: z.string(),
338
+ key: z
339
+ .string()
340
+ .optional()
341
+ .describe("The record's external key, when it has one"),
342
+ })
343
+ .strict();
344
+ /** Graph facts a review item proposes; accepting commits them. */
345
+ export const suggestedFactsSchema = z
346
+ .object({
347
+ triplets: z
348
+ .array(z
349
+ .object({
350
+ source: namedEntityInputSchema,
351
+ relation: z.string(),
352
+ target: namedEntityInputSchema,
353
+ confidence: z.number().min(0).max(1).optional(),
354
+ evidence: z
355
+ .unknown()
356
+ .optional()
357
+ .describe("Why the link holds: a string, or { text, source_id } as lbb_commit takes it"),
358
+ })
359
+ .passthrough())
360
+ .max(200)
361
+ .optional()
362
+ .describe("Links to commit, as lbb_commit takes triplets"),
363
+ entity_properties: z
364
+ .array(jsonObjectSchema)
365
+ .max(200)
366
+ .optional()
367
+ .describe("Fields to commit, each { type, name, properties: { field: value } } as lbb_commit takes them"),
368
+ replace_owned: z
369
+ .object({
370
+ subjects: z
371
+ .array(namedEntityInputSchema)
372
+ .max(1000)
373
+ .describe("The records this write is for"),
374
+ relations: z
375
+ .array(z.string())
376
+ .max(64)
377
+ .optional()
378
+ .describe("Owned relations: their current out-edges of the subjects that the facts do not write again are removed"),
379
+ properties: z
380
+ .array(z.string())
381
+ .max(64)
382
+ .optional()
383
+ .describe("Owned fields: the ones the facts do not set on a subject are removed"),
384
+ retract_entities: z
385
+ .array(namedEntityInputSchema)
386
+ .max(1000)
387
+ .optional()
388
+ .describe("Records the producer made and no longer makes"),
389
+ })
390
+ .strict()
391
+ .optional()
392
+ .describe("Replace the producer's owned edges and fields on its subjects in the same commit"),
393
+ summary: z
394
+ .string()
395
+ .max(500)
396
+ .optional()
397
+ .describe("One line about the facts for the reviewer"),
398
+ })
399
+ .strict()
400
+ .describe("Graph facts a person confirms: accepting commits them after the change (at most 200 triplets and property rows together). Records graphs only");
334
401
  export const inspectInputSchema = z.discriminatedUnion("action", [
335
402
  z.object({ action: z.literal("guide"), ...readScope }).strict(),
336
403
  z
@@ -405,6 +472,17 @@ export const SPARQL_IRI_GUIDE = 'IRI scheme: relations are <https://littlebigbra
405
472
  // Search by meaning runs inside SPARQL text as a pattern, so an agent can
406
473
  // filter and join the hits in one query instead of a search then a lookup.
407
474
  export const SPARQL_SEARCH_GUIDE = 'Search by meaning inside the query (the graph needs an embedding; see lbb_embeddings action=list): PREFIX search: <https://littlebigbrain.com/search#> SELECT ?x ?score WHERE { ?x search:similarTo "card payments" ; search:score ?score . ?x <https://littlebigbrain.com/r/calls> ?y } ORDER BY DESC(?score) LIMIT 5. The other patterns filter and join the hits. One search:similarTo per query, with a variable subject, in the main group (not inside OPTIONAL, UNION, MINUS or a subquery). The object is text, an entity IRI (records like that one) or a vector literal. search:top N sets the hits (1 to 1000; default the query LIMIT, then 10) and search:embedding "name" searches one embedding. FILTER(?score > 0.8) keeps close hits. The result\'s search field reports the plan (nearest, filter_first or search_first), the hits and complete. For a question in words, add search:rerank true ; search:relevance ?r and ORDER BY DESC(?r): the rerank model (Jev) reads the best hits with the text, keeps the top that answer it and binds its answer (0 to 1); search.rerank reports the status. Only a text query reranks.';
475
+ /** One point of `lbb_query mode=compare`: a commit, a date or a moment. */
476
+ const comparePointSchema = z
477
+ .object({
478
+ as_of_commit_seq: z.number().int().min(0).optional(),
479
+ date: z
480
+ .string()
481
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
482
+ .optional(),
483
+ moment: z.string().optional(),
484
+ })
485
+ .strict();
408
486
  export const queryInputSchema = z.discriminatedUnion("mode", [
409
487
  z
410
488
  .object({
@@ -447,7 +525,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
447
525
  query: z
448
526
  .string()
449
527
  .optional()
450
- .describe(`SPARQL 1.1 query text (SELECT or ASK). Valid-time as_of is unsupported; use as_of_commit_seq for a retained commit snapshot. ${SPARQL_IRI_GUIDE} Example: SELECT ?service ?db WHERE { ?service <https://littlebigbrain.com/r/writes_to> ?db } LIMIT 10. ${SPARQL_SEARCH_GUIDE}`),
528
+ .describe(`SPARQL 1.1 query text (SELECT or ASK). Valid-time as_of is unsupported; use as_of_commit_seq for a retained commit snapshot. ${SPARQL_IRI_GUIDE} Example: SELECT ?service ?db WHERE { ?service <https://littlebigbrain.com/r/writes_to> ?db } LIMIT 10. ${SPARQL_SEARCH_GUIDE} compare: the SELECT to run at both points; leave out LIMIT.`),
451
529
  // Kept for an actionable error when an older connector sends this field.
452
530
  as_of: z
453
531
  .string()
@@ -474,7 +552,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
474
552
  question: z
475
553
  .string()
476
554
  .min(1)
477
- .describe("question: the question in plain words (required), 1 to 4,000 characters. The server writes the SPARQL query from a description of this graph, checks it, and runs it."),
555
+ .describe("question: the question in plain words (required), 1 to 4,000 characters. The server's model runs queries on this graph, reads their rows, and answers. describe: the question whose classes and properties to describe."),
478
556
  context: z
479
557
  .string()
480
558
  .optional()
@@ -489,23 +567,19 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
489
567
  "unanswerable",
490
568
  ])
491
569
  .optional()
492
- .describe("question: the kind of query, when you know it. Omit it and the router model selects it."),
570
+ .describe("question: the kind of question, when you know it. Omit it and the router model picks it."),
493
571
  limit: z
494
572
  .number()
495
573
  .int()
496
574
  .min(1)
497
575
  .max(QUESTION_MAX_ROWS)
498
576
  .optional()
499
- .describe("question: rows the run returns, 1 to 1,000. Defaults by detail: compact=20, standard=100, full=1000."),
500
- run: z
501
- .boolean()
502
- .optional()
503
- .describe("question: run the query and return its rows. Defaults to true; false returns the query only."),
577
+ .describe("question: rows each query of the server's loop returns, 1 to 1,000. Defaults by detail: compact=20, standard=100, full=1000. names: candidates per name, 1 to 10 (default 5). compare: entries of each list per page, 1 to 1,000 (defaults by detail: 20, 100, 500)."),
504
578
  anchor: z
505
579
  .array(z.string().min(1).max(QUESTION_MAX_ANCHOR_CHARS))
506
580
  .max(QUESTION_MAX_ANCHORS)
507
581
  .optional()
508
- .describe("question: entity IRIs the question is about, at most 10 (for example the record the user has open). The server reads each one, and the query uses these IRIs directly instead of matching their names."),
582
+ .describe("question: entity IRIs the question is about, at most 10 (for example the record the user has open). The server reads each one, and its queries use these IRIs directly instead of matching their names."),
509
583
  timeline: z
510
584
  .array(z
511
585
  .object({
@@ -541,7 +615,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
541
615
  text: z
542
616
  .string()
543
617
  .optional()
544
- .describe("The query text (required); embedded with the graph's model."),
618
+ .describe("search: the query text (required); embedded with the graph's model. names: the question, or the names, to find (required)."),
545
619
  top_k: z.number().int().positive().max(200).optional(),
546
620
  probe: z
547
621
  .number()
@@ -605,6 +679,82 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
605
679
  ...readScope,
606
680
  })
607
681
  .strict(),
682
+ z
683
+ .object({
684
+ mode: z.literal("names"),
685
+ text: z
686
+ .string()
687
+ .min(1)
688
+ .max(4_000)
689
+ .describe("names: the question, or the names, to find (required)."),
690
+ limit: z.number().int().min(1).max(10).optional(),
691
+ ...readScope,
692
+ })
693
+ .strict(),
694
+ z
695
+ .object({
696
+ mode: z.literal("describe"),
697
+ question: z.string().max(4_000).optional(),
698
+ classes: z
699
+ .array(z.string().min(1))
700
+ .max(20)
701
+ .optional()
702
+ .describe("describe: class IRIs to describe, at most 20."),
703
+ properties: z
704
+ .array(z.string().min(1))
705
+ .max(50)
706
+ .optional()
707
+ .describe("describe: property IRIs to describe, at most 50."),
708
+ ...readScope,
709
+ })
710
+ .strict(),
711
+ z
712
+ .object({
713
+ mode: z.literal("commit_at"),
714
+ date: z
715
+ .string()
716
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
717
+ .optional()
718
+ .describe("commit_at: YYYY-MM-DD; the last commit written by the end of that day (UTC)."),
719
+ moment: z
720
+ .string()
721
+ .optional()
722
+ .describe("commit_at: RFC 3339, e.g. 2026-06-18T12:00:00Z; the last commit written at or before it."),
723
+ ...readScope,
724
+ })
725
+ .strict(),
726
+ z
727
+ .object({
728
+ mode: z.literal("compare"),
729
+ query: z
730
+ .string()
731
+ .min(1)
732
+ .describe("compare: the SELECT to run at both points. Leave out LIMIT: the server reads up to max_rows rows a point, page by page."),
733
+ before: comparePointSchema.describe('compare: the earlier point (required): {"as_of_commit_seq": n}, {"date": "YYYY-MM-DD"} or {"moment": "RFC 3339"}.'),
734
+ after: comparePointSchema
735
+ .optional()
736
+ .describe("compare: the later point; defaults to the latest commit."),
737
+ key: z
738
+ .array(z.string().min(1))
739
+ .max(8)
740
+ .optional()
741
+ .describe('compare: the variables that identify a row\'s entity, e.g. ["contact"]. With a key the rows are paired: added and removed hold the entities at one point only, changed the entities whose values differ. Without one whole rows are compared.'),
742
+ entailment: z.enum(["none", "subclass", "rdfs", "owl"]).optional(),
743
+ max_rows: z
744
+ .number()
745
+ .int()
746
+ .min(1)
747
+ .max(50_000)
748
+ .optional()
749
+ .describe("compare: rows to read per point, 1 to 50,000 (default 20,000). Past it the result says truncated."),
750
+ limit: z.number().int().min(1).max(1_000).optional(),
751
+ compare_cursor: z
752
+ .string()
753
+ .optional()
754
+ .describe("compare: the cursor of the page before, from next; pass it with the same arguments."),
755
+ ...readScope,
756
+ })
757
+ .strict(),
608
758
  ]);
609
759
  export const configureInputSchema = z.discriminatedUnion("action", [
610
760
  z
@@ -690,9 +840,10 @@ export const configureInputSchema = z.discriminatedUnion("action", [
690
840
  .describe("Short imperative text, e.g. 'Add class Contractor'"),
691
841
  change: z
692
842
  .array(ontologyEvolveOpSchema)
693
- .min(1)
694
843
  .max(128)
695
- .describe("The ontology operations a person applies by accepting, in order (same shapes as evolve_ontology ops)"),
844
+ .optional()
845
+ .describe("The ontology operations a person applies by accepting, in order (same shapes as evolve_ontology ops). Up to 128; may be empty or absent when facts is set"),
846
+ facts: suggestedFactsSchema.optional(),
696
847
  rationale: z
697
848
  .string()
698
849
  .max(4000)