@littlebigbrain/mcp 0.15.1 → 0.16.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/README.md +5 -0
- package/dist/approval.d.ts +139 -0
- package/dist/approval.js +426 -0
- package/dist/files-tool.d.ts +2 -2
- package/dist/files-tool.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/query-observer.d.ts +37 -0
- package/dist/rdf-tool.d.ts +2 -2
- package/dist/rdf-tool.js +18 -4
- package/dist/reviews-tool.d.ts +3 -0
- package/dist/reviews-tool.js +420 -0
- package/dist/sparql-update.d.ts +15 -0
- package/dist/sparql-update.js +146 -0
- package/dist/tool-contracts.js +227 -16
- package/dist/tool-runtime.js +91 -5
- package/dist/tools.js +761 -273
- package/package.json +3 -3
package/dist/tool-contracts.js
CHANGED
|
@@ -11,12 +11,6 @@ export const QUESTION_MAX_ANCHOR_CHARS = 2_000;
|
|
|
11
11
|
export const QUESTION_MAX_TIMELINE = 200;
|
|
12
12
|
export const QUESTION_MAX_TIMELINE_LABEL_CHARS = 200;
|
|
13
13
|
export const READ_ONLY = { readOnlyHint: true };
|
|
14
|
-
export const IDEMPOTENT_WRITE = {
|
|
15
|
-
readOnlyHint: false,
|
|
16
|
-
destructiveHint: false,
|
|
17
|
-
idempotentHint: true,
|
|
18
|
-
openWorldHint: false,
|
|
19
|
-
};
|
|
20
14
|
export const MUTATING = {
|
|
21
15
|
readOnlyHint: false,
|
|
22
16
|
destructiveHint: false,
|
|
@@ -132,7 +126,9 @@ export const searchFeedbackSchema = z
|
|
|
132
126
|
query: z.string().min(1),
|
|
133
127
|
search_id: z.string().optional(),
|
|
134
128
|
snapshot: z.record(z.string(), z.unknown()).optional(),
|
|
135
|
-
profile: z
|
|
129
|
+
profile: z
|
|
130
|
+
.enum(["baseline", "scored_atom_v1", "graph_aware_v1", "ndcg_v1"])
|
|
131
|
+
.nullish(),
|
|
136
132
|
model_id: z.string().optional(),
|
|
137
133
|
labeler_id: z.string().optional(),
|
|
138
134
|
labels: z
|
|
@@ -259,6 +255,34 @@ export const ontologyEvolveOpSchema = z.discriminatedUnion("op", [
|
|
|
259
255
|
.boolean()
|
|
260
256
|
.optional()
|
|
261
257
|
.describe("Advisory required flag (not enforced on commit)"),
|
|
258
|
+
unit: z
|
|
259
|
+
.string()
|
|
260
|
+
.optional()
|
|
261
|
+
.describe("Unit of an i64 or f64 field, e.g. mm, cm, kg, m2 or h. Writers that read text, such as values.extract, convert numbers to it"),
|
|
262
|
+
allowed_values: z
|
|
263
|
+
.array(z.string())
|
|
264
|
+
.optional()
|
|
265
|
+
.describe("Enforced allowed values (sh:in): a commit with another value for the field is refused. For keyword, text, i64 and bool fields only"),
|
|
266
|
+
})
|
|
267
|
+
.strict(),
|
|
268
|
+
z
|
|
269
|
+
.object({
|
|
270
|
+
op: z.literal("set_property_unit"),
|
|
271
|
+
property: z.string().describe("An existing i64 or f64 field, by name"),
|
|
272
|
+
unit: z
|
|
273
|
+
.string()
|
|
274
|
+
.optional()
|
|
275
|
+
.describe("The unit's symbol, as add_property takes it; omit it to clear the unit. Stored values are not converted"),
|
|
276
|
+
})
|
|
277
|
+
.strict(),
|
|
278
|
+
z
|
|
279
|
+
.object({
|
|
280
|
+
op: z.literal("set_property_constraint"),
|
|
281
|
+
property: z.string().describe("An existing field, by name"),
|
|
282
|
+
allowed_values: z
|
|
283
|
+
.array(z.string())
|
|
284
|
+
.optional()
|
|
285
|
+
.describe("The allowed values that later commits are checked against; empty or absent clears the constraint. Stored values are not checked again"),
|
|
262
286
|
})
|
|
263
287
|
.strict(),
|
|
264
288
|
z
|
|
@@ -331,16 +355,54 @@ export const ontologyEvolveOpSchema = z.discriminatedUnion("op", [
|
|
|
331
355
|
})
|
|
332
356
|
.strict(),
|
|
333
357
|
]);
|
|
358
|
+
// The inputs below are as loose as the server: an optional field takes
|
|
359
|
+
// null, and a field the server does not know passes through (the server
|
|
360
|
+
// ignores it). 0.15 callers send `source_id: null` and `key: null`.
|
|
361
|
+
/** A box on a page: 0-based page, coordinates normalized to the page (0 to 1). */
|
|
362
|
+
export const regionSchema = z
|
|
363
|
+
.object({
|
|
364
|
+
page: z.number().int().nonnegative(),
|
|
365
|
+
x0: z.number().min(0).max(1),
|
|
366
|
+
y0: z.number().min(0).max(1),
|
|
367
|
+
x1: z.number().min(0).max(1),
|
|
368
|
+
y1: z.number().min(0).max(1),
|
|
369
|
+
confidence: z.number().min(0).max(1),
|
|
370
|
+
})
|
|
371
|
+
.passthrough();
|
|
372
|
+
/** Why a link holds: a string, or the text with its source and page box. */
|
|
373
|
+
export const evidenceSchema = z.union([
|
|
374
|
+
z.string(),
|
|
375
|
+
z
|
|
376
|
+
.object({
|
|
377
|
+
text: z.string().nullish(),
|
|
378
|
+
source_id: z.string().nullish(),
|
|
379
|
+
region: regionSchema.nullish(),
|
|
380
|
+
observation_id: z.string().nullish(),
|
|
381
|
+
})
|
|
382
|
+
.passthrough(),
|
|
383
|
+
]);
|
|
384
|
+
/** A vector you computed for one record. */
|
|
385
|
+
export const entityEmbeddingSchema = z
|
|
386
|
+
.object({
|
|
387
|
+
type: z.string(),
|
|
388
|
+
name: z.string(),
|
|
389
|
+
key: z.string().nullish(),
|
|
390
|
+
entity_id: z.string().nullish(),
|
|
391
|
+
model_id: z.string(),
|
|
392
|
+
vector: z.array(z.number()),
|
|
393
|
+
metric: z.enum(["cosine", "dot", "l2"]).nullish(),
|
|
394
|
+
})
|
|
395
|
+
.passthrough();
|
|
334
396
|
const namedEntityInputSchema = z
|
|
335
397
|
.object({
|
|
336
398
|
type: z.string(),
|
|
337
399
|
name: z.string(),
|
|
338
400
|
key: z
|
|
339
401
|
.string()
|
|
340
|
-
.
|
|
402
|
+
.nullish()
|
|
341
403
|
.describe("The record's external key, when it has one"),
|
|
342
404
|
})
|
|
343
|
-
.
|
|
405
|
+
.passthrough();
|
|
344
406
|
/** Graph facts a review item proposes; accepting commits them. */
|
|
345
407
|
export const suggestedFactsSchema = z
|
|
346
408
|
.object({
|
|
@@ -351,9 +413,8 @@ export const suggestedFactsSchema = z
|
|
|
351
413
|
relation: z.string(),
|
|
352
414
|
target: namedEntityInputSchema,
|
|
353
415
|
confidence: z.number().min(0).max(1).optional(),
|
|
354
|
-
evidence:
|
|
355
|
-
.
|
|
356
|
-
.optional()
|
|
416
|
+
evidence: evidenceSchema
|
|
417
|
+
.nullish()
|
|
357
418
|
.describe("Why the link holds: a string, or { text, source_id } as lbb_commit takes it"),
|
|
358
419
|
})
|
|
359
420
|
.passthrough())
|
|
@@ -398,6 +459,127 @@ export const suggestedFactsSchema = z
|
|
|
398
459
|
})
|
|
399
460
|
.strict()
|
|
400
461
|
.describe("Graph facts a person confirms: accepting commits them after the change (at most 200 triplets and property rows together). Records graphs only");
|
|
462
|
+
/** The facts as the API takes them. */
|
|
463
|
+
export function suggestedFactsBody(facts) {
|
|
464
|
+
return {
|
|
465
|
+
...facts,
|
|
466
|
+
// Rows pass through as lbb_commit takes them; the server coerces each
|
|
467
|
+
// value to its field's declared type.
|
|
468
|
+
entity_properties: facts.entity_properties,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/** A person's answer to a review item, or to one value of a workflow test. */
|
|
472
|
+
export const reviewAnswerSchema = z
|
|
473
|
+
.object({
|
|
474
|
+
decision: z
|
|
475
|
+
.enum(["accept", "reject", "correct", "choose", "cant_tell"])
|
|
476
|
+
.describe("accept, reject, correct (with value), choose (with option) or cant_tell"),
|
|
477
|
+
value: z
|
|
478
|
+
.union([z.number(), z.string(), z.boolean()])
|
|
479
|
+
.optional()
|
|
480
|
+
.describe("correct: the right value, in the property's type. A number may name its unit, e.g. 450 mm"),
|
|
481
|
+
option: z
|
|
482
|
+
.string()
|
|
483
|
+
.optional()
|
|
484
|
+
.describe("choose: the id of the chosen option or grade"),
|
|
485
|
+
note: z
|
|
486
|
+
.string()
|
|
487
|
+
.max(1000)
|
|
488
|
+
.optional()
|
|
489
|
+
.describe("Why, in a few words; for an answer item, the answer text. At most 1,000 characters"),
|
|
490
|
+
})
|
|
491
|
+
.strict();
|
|
492
|
+
/** A workflow to test before it exists: the create request of a values.extract workflow. */
|
|
493
|
+
export const testInstallSchema = z
|
|
494
|
+
.object({
|
|
495
|
+
starter: z.string().describe("The starter workflow, values.extract"),
|
|
496
|
+
name: z
|
|
497
|
+
.string()
|
|
498
|
+
.optional()
|
|
499
|
+
.describe("The name the workflow would get; default the class's local name"),
|
|
500
|
+
watch: z
|
|
501
|
+
.object({
|
|
502
|
+
class: z.string().describe("The class whose instances it reads"),
|
|
503
|
+
fields: z
|
|
504
|
+
.array(z.string())
|
|
505
|
+
.optional()
|
|
506
|
+
.describe("The fields that hold the text, as embedding paths"),
|
|
507
|
+
exclude: z.array(z.string()).optional(),
|
|
508
|
+
})
|
|
509
|
+
.strict()
|
|
510
|
+
.optional(),
|
|
511
|
+
params: jsonObjectSchema
|
|
512
|
+
.optional()
|
|
513
|
+
.describe("The starter workflow's settings, as its catalog entry lists them (the properties to fill, the context)"),
|
|
514
|
+
gate: z
|
|
515
|
+
.object({
|
|
516
|
+
below: z
|
|
517
|
+
.number()
|
|
518
|
+
.min(0)
|
|
519
|
+
.max(1)
|
|
520
|
+
.describe("Values below this support go to review"),
|
|
521
|
+
})
|
|
522
|
+
.strict()
|
|
523
|
+
.optional(),
|
|
524
|
+
})
|
|
525
|
+
.strict();
|
|
526
|
+
/** One label of `lbb_evals action=label`: a result and whether it answers the request. */
|
|
527
|
+
export const evalLabelItemSchema = z
|
|
528
|
+
.object({
|
|
529
|
+
id: z.string(),
|
|
530
|
+
valid: z.boolean(),
|
|
531
|
+
note: z.string().optional(),
|
|
532
|
+
})
|
|
533
|
+
.strict();
|
|
534
|
+
/** A value a correct run writes, or, in leave_out, one it must not write. */
|
|
535
|
+
const evalExpectedValueSchema = z
|
|
536
|
+
.object({
|
|
537
|
+
property: z.string(),
|
|
538
|
+
value: z
|
|
539
|
+
.union([z.number(), z.string(), z.boolean(), z.null()])
|
|
540
|
+
.describe("Numbers compare as numbers, text in lower case. In leave_out, null means no value at all"),
|
|
541
|
+
accept: z
|
|
542
|
+
.array(z.string())
|
|
543
|
+
.optional()
|
|
544
|
+
.describe("Other spellings a text value may take, lower case"),
|
|
545
|
+
stated: z
|
|
546
|
+
.string()
|
|
547
|
+
.optional()
|
|
548
|
+
.describe("The number and unit as the text states them, e.g. 450 mm"),
|
|
549
|
+
quote: z
|
|
550
|
+
.string()
|
|
551
|
+
.optional()
|
|
552
|
+
.describe("Words of the text that state the value"),
|
|
553
|
+
reason: z.string().optional().describe("Why a value is left out"),
|
|
554
|
+
})
|
|
555
|
+
.strict();
|
|
556
|
+
/** One example of a values dataset. */
|
|
557
|
+
export const evalDatasetItemSchema = z
|
|
558
|
+
.object({
|
|
559
|
+
name: z
|
|
560
|
+
.string()
|
|
561
|
+
.describe("The input's name, unique in the dataset; the same name replaces"),
|
|
562
|
+
text: z
|
|
563
|
+
.string()
|
|
564
|
+
.max(40_000)
|
|
565
|
+
.optional()
|
|
566
|
+
.describe("The input text, at most 40,000 characters"),
|
|
567
|
+
values: z.array(evalExpectedValueSchema).optional(),
|
|
568
|
+
leave_out: z.array(evalExpectedValueSchema).optional(),
|
|
569
|
+
iri: z.string().optional().describe("The instance the input came from"),
|
|
570
|
+
case: z
|
|
571
|
+
.string()
|
|
572
|
+
.optional()
|
|
573
|
+
.describe("What the example shows, in one sentence"),
|
|
574
|
+
source: z
|
|
575
|
+
.object({
|
|
576
|
+
kind: z.enum(["test", "review", "answer_key"]),
|
|
577
|
+
id: z.string().optional(),
|
|
578
|
+
})
|
|
579
|
+
.strict()
|
|
580
|
+
.optional(),
|
|
581
|
+
})
|
|
582
|
+
.strict();
|
|
401
583
|
export const inspectInputSchema = z.discriminatedUnion("action", [
|
|
402
584
|
z.object({ action: z.literal("guide"), ...readScope }).strict(),
|
|
403
585
|
z
|
|
@@ -444,6 +626,36 @@ export const inspectInputSchema = z.discriminatedUnion("action", [
|
|
|
444
626
|
z
|
|
445
627
|
.object({
|
|
446
628
|
action: z.literal("entity"),
|
|
629
|
+
version: z.literal("records/1").optional(),
|
|
630
|
+
iri: z
|
|
631
|
+
.string()
|
|
632
|
+
.optional()
|
|
633
|
+
.describe("Authoritative full RDF IRI; selects the lossless records/1 read."),
|
|
634
|
+
key: z
|
|
635
|
+
.string()
|
|
636
|
+
.optional()
|
|
637
|
+
.describe("Stable key with entity_type, resolved from the activated schema at the read snapshot."),
|
|
638
|
+
schema_revision: z
|
|
639
|
+
.string()
|
|
640
|
+
.optional()
|
|
641
|
+
.describe("Expected opaque schema revision for a records/1 read."),
|
|
642
|
+
consistency: z.enum(["strong", "eventual"]).optional(),
|
|
643
|
+
limits: z
|
|
644
|
+
.object({
|
|
645
|
+
properties: z.number().int().positive().max(10_000).optional(),
|
|
646
|
+
outgoing: z.number().int().positive().max(10_000).optional(),
|
|
647
|
+
incoming: z.number().int().positive().max(10_000).optional(),
|
|
648
|
+
assertions: z.number().int().positive().max(100).optional(),
|
|
649
|
+
assertion_statements: z
|
|
650
|
+
.number()
|
|
651
|
+
.int()
|
|
652
|
+
.positive()
|
|
653
|
+
.max(10_000)
|
|
654
|
+
.optional(),
|
|
655
|
+
})
|
|
656
|
+
.strict()
|
|
657
|
+
.optional()
|
|
658
|
+
.describe("Lossless records/1 section bounds. Exceeding a bound refuses the read instead of truncating it."),
|
|
447
659
|
entity_id: z
|
|
448
660
|
.string()
|
|
449
661
|
.optional()
|
|
@@ -465,10 +677,9 @@ export const inspectInputSchema = z.discriminatedUnion("action", [
|
|
|
465
677
|
})
|
|
466
678
|
.strict(),
|
|
467
679
|
]);
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
export const SPARQL_IRI_GUIDE = 'IRI scheme: relations are <https://littlebigbrain.com/r/NAME> (NAME lowercased, e.g. writes_to; reverse a relation with the ^ path operator, no stored inverse triple). Types are <https://littlebigbrain.com/class/NAME> (lowercased), matched as `?x a <…/class/NAME>` with explicit entailment=subclass, rdfs, or owl for inference (default none). Property fields are <https://littlebigbrain.com/p/NAME> (lowercased). The local name is ALWAYS lowercase — an uppercase one (e.g. <…/r/FOR_CLIENT>) is a different, non-existent IRI that silently matches nothing; this tool auto-lowercases the local name of /r/, /class/, and /p/ IRIs for you and adds a `notes` entry when it does, so a stray uppercase still resolves. (Structured mode\'s `predicate` is case-insensitive on its own.) Entities are content-addressed <https://littlebigbrain.com/e/HASH> — never build an entity IRI from a name; anchor a named entity by its label instead: `?e <http://www.w3.org/2000/01/rdf-schema#label> "Acme"`. Discover the exact relation and type names with lbb_inspect action=ontology. SELECT and ASK only (CONSTRUCT/DESCRIBE are rejected).';
|
|
680
|
+
// Native records use a conventional projection; imported RDF keeps its exact IRIs.
|
|
681
|
+
// Explain both so agents do not remint or normalize arbitrary vocabulary.
|
|
682
|
+
export const SPARQL_IRI_GUIDE = 'Imported RDF retains its exact, case-sensitive IRIs. Use returned IRIs unchanged; lbb_inspect action=entity iri=<full IRI> reads any subject with records/1. The following convention applies only to native records: relations are <https://littlebigbrain.com/r/NAME> (NAME lowercased, e.g. writes_to; reverse a relation with the ^ path operator, no stored inverse triple). Types are <https://littlebigbrain.com/class/NAME> (lowercased), matched as `?x a <…/class/NAME>` with explicit entailment=subclass, rdfs, or owl for inference (default none). Property fields are <https://littlebigbrain.com/p/NAME> (lowercased). The local name is ALWAYS lowercase — an uppercase one (e.g. <…/r/FOR_CLIENT>) is a different, non-existent IRI that silently matches nothing; this tool auto-lowercases the local name of /r/, /class/, and /p/ IRIs for you and adds a `notes` entry when it does, so a stray uppercase still resolves. (Structured mode\'s `predicate` is case-insensitive on its own.) Entities are content-addressed <https://littlebigbrain.com/e/HASH> — never build an entity IRI from a name; anchor a named entity by its label instead: `?e <http://www.w3.org/2000/01/rdf-schema#label> "Acme"`. Discover the exact relation and type names with lbb_inspect action=ontology. SELECT and ASK only (CONSTRUCT/DESCRIBE are rejected).';
|
|
472
683
|
// Search by meaning runs inside SPARQL text as a pattern, so an agent can
|
|
473
684
|
// filter and join the hits in one query instead of a search then a lookup.
|
|
474
685
|
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.';
|
package/dist/tool-runtime.js
CHANGED
|
@@ -212,6 +212,18 @@ export function compactLimits(detail) {
|
|
|
212
212
|
return { maxItems: 5, maxString: 300 };
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Items one list page asks for: `limit`, at most `most` (the route's own
|
|
217
|
+
* cap) and at most the items the detail level shows (5, 20 or 100), so the
|
|
218
|
+
* detail level never cuts the page. A page over the output budget
|
|
219
|
+
* (HARD_OUTPUT_CHARS) is cut too: `runPage` then asks the server again for
|
|
220
|
+
* the items that fit, so next_cursor continues after the last item the
|
|
221
|
+
* caller saw.
|
|
222
|
+
*/
|
|
223
|
+
export function pageLimit(detail, limit, most) {
|
|
224
|
+
const shown = compactLimits(normalizeDetail(detail)).maxItems;
|
|
225
|
+
return Math.min(limit ?? shown, shown, most);
|
|
226
|
+
}
|
|
215
227
|
export function truncateValue(value, limits, state) {
|
|
216
228
|
if (typeof value === "string") {
|
|
217
229
|
if (value.length <= limits.maxString)
|
|
@@ -267,10 +279,75 @@ export function defaultSummary(label, value, truncated) {
|
|
|
267
279
|
: "";
|
|
268
280
|
return `${label}${countText}${truncated ? " [truncated]" : ""}`;
|
|
269
281
|
}
|
|
282
|
+
/** The next_cursor of a list page, kept whole: a cut cursor is no cursor. */
|
|
283
|
+
function keepCursor(value, data) {
|
|
284
|
+
const cursor = value?.next_cursor;
|
|
285
|
+
return typeof cursor === "string" && data && typeof data === "object"
|
|
286
|
+
? { ...data, next_cursor: cursor }
|
|
287
|
+
: data;
|
|
288
|
+
}
|
|
289
|
+
/** The items of a list page (`{data, has_more, next_cursor}`), else undefined. */
|
|
290
|
+
function pageItems(value) {
|
|
291
|
+
if (!value || typeof value !== "object")
|
|
292
|
+
return undefined;
|
|
293
|
+
const page = value;
|
|
294
|
+
return Array.isArray(page.data) &&
|
|
295
|
+
("next_cursor" in page || "has_more" in page)
|
|
296
|
+
? page.data
|
|
297
|
+
: undefined;
|
|
298
|
+
}
|
|
299
|
+
/** Whether the envelope of `value` fits the output budget without the hard cap. */
|
|
300
|
+
function fitsOutput(label, value, detailArg) {
|
|
301
|
+
const detail = normalizeDetail(detailArg);
|
|
302
|
+
const state = { truncated: false };
|
|
303
|
+
const data = keepCursor(value, truncateValue(value, compactLimits(detail), state));
|
|
304
|
+
const text = JSON.stringify({
|
|
305
|
+
summary: defaultSummary(label, value, state.truncated),
|
|
306
|
+
data,
|
|
307
|
+
counts: countsFor(value),
|
|
308
|
+
truncated: state.truncated || undefined,
|
|
309
|
+
next: state.truncated ? { detail: nextDetail(detail) } : undefined,
|
|
310
|
+
}, null, 2);
|
|
311
|
+
// A margin for the fields the envelope adds beside the page.
|
|
312
|
+
return text.length <= HARD_OUTPUT_CHARS - 512;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Run a call that answers one list page. `fetch(limit)` asks the server
|
|
316
|
+
* for at most `limit` items after the call's cursor. When the page does
|
|
317
|
+
* not fit the output budget, the envelope would cut it to a few items
|
|
318
|
+
* while next_cursor points after the last item asked for; so the server
|
|
319
|
+
* is asked again for the items that fit, and next_cursor continues after
|
|
320
|
+
* the last item shown.
|
|
321
|
+
*/
|
|
322
|
+
export async function runPage(client, label, detail, limit, fetch) {
|
|
323
|
+
try {
|
|
324
|
+
let page = await fetch(limit);
|
|
325
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
326
|
+
const items = pageItems(page);
|
|
327
|
+
if (!items || items.length <= 1 || fitsOutput(label, page, detail))
|
|
328
|
+
break;
|
|
329
|
+
let lo = 1;
|
|
330
|
+
let hi = items.length - 1;
|
|
331
|
+
while (lo < hi) {
|
|
332
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
333
|
+
const fits = fitsOutput(label, { ...page, data: items.slice(0, mid) }, detail);
|
|
334
|
+
if (fits)
|
|
335
|
+
lo = mid;
|
|
336
|
+
else
|
|
337
|
+
hi = mid - 1;
|
|
338
|
+
}
|
|
339
|
+
page = await fetch(lo);
|
|
340
|
+
}
|
|
341
|
+
return toolResult(envelope(label, page, detail));
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
return errorResult(await enrichError(client, error));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
270
347
|
export function envelope(label, value, detailArg, next) {
|
|
271
348
|
const detail = normalizeDetail(detailArg);
|
|
272
349
|
const state = { truncated: false };
|
|
273
|
-
let data = truncateValue(value, compactLimits(detail), state);
|
|
350
|
+
let data = keepCursor(value, truncateValue(value, compactLimits(detail), state));
|
|
274
351
|
let result = {
|
|
275
352
|
summary: defaultSummary(label, value, state.truncated),
|
|
276
353
|
data,
|
|
@@ -284,7 +361,7 @@ export function envelope(label, value, detailArg, next) {
|
|
|
284
361
|
if (text.length <= HARD_OUTPUT_CHARS)
|
|
285
362
|
return result;
|
|
286
363
|
const hardState = { truncated: true };
|
|
287
|
-
data = truncateValue(value, { maxItems: 3, maxString: 120 }, hardState);
|
|
364
|
+
data = keepCursor(value, truncateValue(value, { maxItems: 3, maxString: 120 }, hardState));
|
|
288
365
|
result = {
|
|
289
366
|
summary: `${label} [hard-capped for MCP output]`,
|
|
290
367
|
data,
|
|
@@ -297,10 +374,10 @@ export function envelope(label, value, detailArg, next) {
|
|
|
297
374
|
return result;
|
|
298
375
|
return {
|
|
299
376
|
summary: `${label} [hard-capped for MCP output]`,
|
|
300
|
-
data: {
|
|
377
|
+
data: keepCursor(value, {
|
|
301
378
|
note: "The response was too large for the MCP tool result. Narrow the query or request a smaller top_k.",
|
|
302
379
|
preview: text.slice(0, 20_000),
|
|
303
|
-
},
|
|
380
|
+
}),
|
|
304
381
|
counts: countsFor(value),
|
|
305
382
|
truncated: true,
|
|
306
383
|
next: { ...(next ?? {}), detail: "full" },
|
|
@@ -404,6 +481,14 @@ export function toolResult(value) {
|
|
|
404
481
|
structuredContent: value,
|
|
405
482
|
};
|
|
406
483
|
}
|
|
484
|
+
/** An error the tool raises itself, with its own code (`approval_declined`). */
|
|
485
|
+
export class ToolError extends Error {
|
|
486
|
+
code;
|
|
487
|
+
constructor(code, message) {
|
|
488
|
+
super(message);
|
|
489
|
+
this.code = code;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
407
492
|
export function errorResult(error) {
|
|
408
493
|
const payload = error instanceof LbbError
|
|
409
494
|
? {
|
|
@@ -417,7 +502,7 @@ export function errorResult(error) {
|
|
|
417
502
|
}
|
|
418
503
|
: {
|
|
419
504
|
type: "tool_error",
|
|
420
|
-
code: "tool_error",
|
|
505
|
+
code: error instanceof ToolError ? error.code : "tool_error",
|
|
421
506
|
message: error instanceof Error ? error.message : String(error),
|
|
422
507
|
param: null,
|
|
423
508
|
request_id: null,
|
|
@@ -709,6 +794,7 @@ export async function analyze(scopedClient, p) {
|
|
|
709
794
|
}
|
|
710
795
|
else if (metric === "sparql") {
|
|
711
796
|
const body = requireObject(p.sparql, "sparql");
|
|
797
|
+
// The body is the caller's JSON; the server checks it.
|
|
712
798
|
const res = (await scopedClient.sparql(body));
|
|
713
799
|
const groups = res.groups ?? [];
|
|
714
800
|
const first = groups[0];
|