@modelprofile.com/flexharness 10.14.0 → 10.15.1
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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.js +4 -2
- package/dist_ts/classes.toolcatalog.d.ts +13 -3
- package/dist_ts/classes.toolcatalog.js +239 -59
- package/dist_ts/interfaces.d.ts +49 -3
- package/dist_ts/interfaces.js +11 -1
- package/dist_ts/utils.harnessoptions.d.ts +2 -0
- package/dist_ts/utils.harnessoptions.js +19 -6
- package/dist_ts_document/index.js +21 -8
- package/dist_ts_document/plugins.d.ts +2 -2
- package/dist_ts_document/plugins.js +3 -3
- package/dist_ts_media/index.js +14 -3
- package/dist_ts_media/plugins.d.ts +2 -2
- package/dist_ts_media/plugins.js +3 -3
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +4 -1
- package/ts/classes.toolcatalog.ts +253 -56
- package/ts/interfaces.ts +50 -3
- package/ts/utils.harnessoptions.ts +26 -3
- package/ts_document/index.ts +18 -7
- package/ts_document/plugins.ts +2 -2
- package/ts_document/readme.md +2 -0
- package/ts_media/index.ts +11 -2
- package/ts_media/plugins.ts +2 -2
|
@@ -57,6 +57,7 @@ interface IMethodEntry {
|
|
|
57
57
|
readonly summary: string;
|
|
58
58
|
readonly description?: string;
|
|
59
59
|
readonly tags: readonly string[];
|
|
60
|
+
readonly aliases: readonly string[];
|
|
60
61
|
readonly examples: readonly string[];
|
|
61
62
|
readonly input: ICatalogSchema;
|
|
62
63
|
readonly inputScope: ISchemaScope;
|
|
@@ -68,7 +69,10 @@ interface IMethodEntry {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
interface ITypeEntry {
|
|
72
|
+
/** The identifier the type is declared under. */
|
|
71
73
|
readonly name: string;
|
|
74
|
+
/** The catalog's name for the type, where it is no identifier; `$ref`s name the type by it. */
|
|
75
|
+
readonly schemaName?: string;
|
|
72
76
|
readonly description?: string;
|
|
73
77
|
readonly declaration?: string;
|
|
74
78
|
/** A catalog type's schema. */
|
|
@@ -107,17 +111,28 @@ const reservedWords = new Set([
|
|
|
107
111
|
const controlPattern = /[\u0000-\u001f\u007f]/;
|
|
108
112
|
const definitionRefPattern = /^#\/(\$defs|definitions)\/([^/]+)$/;
|
|
109
113
|
const declaredTypesHeader = '// Declares types: ';
|
|
114
|
+
const declaredTypesLine = /^\/\/ Declares types: (.*)$/gm;
|
|
110
115
|
const identifierScanPattern = /[A-Za-z_$][A-Za-z0-9_$]*/g;
|
|
111
116
|
const methodKeys = new Set([
|
|
112
|
-
'method', 'summary', 'description', 'tags', 'examples', 'inputSchema', 'outputSchema', 'declaration', 'run', 'onCall',
|
|
117
|
+
'method', 'summary', 'description', 'tags', 'aliases', 'examples', 'inputSchema', 'outputSchema', 'declaration', 'run', 'onCall',
|
|
113
118
|
]);
|
|
114
|
-
const catalogKeys = new Set(['methods', 'types', 'onProposals']);
|
|
119
|
+
const catalogKeys = new Set(['methods', 'types', 'vocabulary', 'onProposals']);
|
|
115
120
|
const typeKeys = new Set(['name', 'description', 'schema', 'declaration']);
|
|
116
121
|
const stopWords = new Set([
|
|
117
|
-
'a', 'an', 'and', 'are', 'all', 'as', 'be', 'by', 'for', 'from', '
|
|
122
|
+
'a', 'an', 'and', 'are', 'all', 'as', 'be', 'by', 'for', 'from', 'i', 'in', 'is', 'it', 'me', 'my',
|
|
118
123
|
'of', 'on', 'or', 'that', 'the', 'this', 'to', 'with',
|
|
119
124
|
]);
|
|
120
|
-
const fieldWeights = { name: 3, namespace: 1.5, summary: 2, tags: 2, description: 1 } as const;
|
|
125
|
+
const fieldWeights = { name: 3, aliases: 3, namespace: 1.5, summary: 2, tags: 2, description: 1 } as const;
|
|
126
|
+
/**
|
|
127
|
+
* A method whose name's, or one alias's, every word the query names ranks this much higher: it is
|
|
128
|
+
* what was asked for.
|
|
129
|
+
*/
|
|
130
|
+
const nameCoverageFactor = 1.5;
|
|
131
|
+
/**
|
|
132
|
+
* A top match leads clearly when it is the only one, or when the query names it and it scores this
|
|
133
|
+
* many times the runner-up.
|
|
134
|
+
*/
|
|
135
|
+
const clearMatchMargin = 1.25;
|
|
121
136
|
type TSearchField = keyof typeof fieldWeights;
|
|
122
137
|
const bm25K1 = 1.2;
|
|
123
138
|
const bm25B = 0.75;
|
|
@@ -216,6 +231,13 @@ const searchTerms = (text: string): string[] => text
|
|
|
216
231
|
.filter(Boolean)
|
|
217
232
|
.map(stem);
|
|
218
233
|
|
|
234
|
+
/** The search terms of a name or an alias, which must have some to be matched at all. */
|
|
235
|
+
const namedTermsOf = (text: string, label: string): string[] => {
|
|
236
|
+
const terms = searchTerms(text);
|
|
237
|
+
if (terms.length === 0) throw new FlexHarnessValidationError(`${label} must contain a letter or digit.`);
|
|
238
|
+
return terms;
|
|
239
|
+
};
|
|
240
|
+
|
|
219
241
|
const editDistance = (left: string, right: string, limit: number): number => {
|
|
220
242
|
if (Math.abs(left.length - right.length) > limit) return limit + 1;
|
|
221
243
|
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
@@ -301,6 +323,25 @@ const textOf = (
|
|
|
301
323
|
return value;
|
|
302
324
|
};
|
|
303
325
|
|
|
326
|
+
/** Optional text: absent, or a string that is empty or blank, counts as not given. */
|
|
327
|
+
const optionalTextOf = (
|
|
328
|
+
value: unknown,
|
|
329
|
+
label: string,
|
|
330
|
+
options: { maxCharacters?: number; maxBytes?: number; singleLine?: boolean },
|
|
331
|
+
): string | undefined => value === undefined || (typeof value === 'string' && !value.trim())
|
|
332
|
+
? undefined
|
|
333
|
+
: textOf(value, label, options);
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The identifier a catalog type is declared under: its name where that is one, else the name with
|
|
337
|
+
* each character an identifier cannot hold replaced by `_`, a leading digit or a reserved word
|
|
338
|
+
* escaped with `_`.
|
|
339
|
+
*/
|
|
340
|
+
const typeIdentifierOf = (name: string): string => {
|
|
341
|
+
const identifier = name.replace(/[^A-Za-z0-9_]/g, '_').replace(/^(?=\d)/, '_');
|
|
342
|
+
return reservedWords.has(identifier) ? `${identifier}_` : identifier;
|
|
343
|
+
};
|
|
344
|
+
|
|
304
345
|
const textListOf = (
|
|
305
346
|
value: unknown,
|
|
306
347
|
label: string,
|
|
@@ -314,6 +355,30 @@ const textListOf = (
|
|
|
314
355
|
return value.map((entry, index) => textOf(entry, `${label}[${index}]`, options));
|
|
315
356
|
};
|
|
316
357
|
|
|
358
|
+
/** A vocabulary alias and the catalog words a query that says it searches for as well. */
|
|
359
|
+
interface IVocabularyEntry {
|
|
360
|
+
readonly alias: readonly string[];
|
|
361
|
+
readonly words: readonly string[];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const vocabularyOf = (value: unknown): IVocabularyEntry[] => {
|
|
365
|
+
if (value === undefined) return [];
|
|
366
|
+
if (!isPlainObject(value)) {
|
|
367
|
+
throw new FlexHarnessValidationError('A tool catalog vocabulary must be a plain object of words and their aliases.');
|
|
368
|
+
}
|
|
369
|
+
const words = Object.entries(value);
|
|
370
|
+
if (words.length > limits.maxVocabularyWords) {
|
|
371
|
+
throw new FlexHarnessValidationError(`A tool catalog vocabulary must have at most ${limits.maxVocabularyWords} words.`);
|
|
372
|
+
}
|
|
373
|
+
const textOptions = { maxCharacters: limits.maxAliasCharacters, singleLine: true };
|
|
374
|
+
return words.flatMap(([word, aliases]) => {
|
|
375
|
+
const label = `Tool catalog vocabulary word "${word}"`;
|
|
376
|
+
const terms = namedTermsOf(textOf(word, label, textOptions), label);
|
|
377
|
+
return textListOf(aliases, `${label} aliases`, limits.maxAliases, textOptions)
|
|
378
|
+
.map((alias, index) => ({ alias: namedTermsOf(alias, `${label} aliases[${index}]`), words: terms }));
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
|
|
317
382
|
async function resolveCatalogSchema(schema: unknown, label: string): Promise<ICatalogSchema> {
|
|
318
383
|
let json: unknown;
|
|
319
384
|
let validate: TValidate | undefined;
|
|
@@ -355,10 +420,14 @@ async function resolveCatalogSchema(schema: unknown, label: string): Promise<ICa
|
|
|
355
420
|
class CatalogSearchIndex {
|
|
356
421
|
private readonly postings = new Map<string, Array<{ index: number; weight: number }>>();
|
|
357
422
|
private readonly idf = new Map<string, number>();
|
|
423
|
+
/** The terms of each method's name and of each of its aliases. */
|
|
424
|
+
private readonly nameTerms: ReadonlyArray<ReadonlyArray<readonly string[]>>;
|
|
358
425
|
|
|
359
|
-
constructor(private readonly entries: readonly IMethodEntry[]) {
|
|
426
|
+
constructor(private readonly entries: readonly IMethodEntry[], private readonly vocabulary: readonly IVocabularyEntry[]) {
|
|
427
|
+
this.nameTerms = entries.map((entry) => [entry.name, ...entry.aliases].map((name) => [...new Set(searchTerms(name))]));
|
|
360
428
|
const fieldTexts = (entry: IMethodEntry): Record<TSearchField, string[]> => ({
|
|
361
429
|
name: searchTerms(entry.name),
|
|
430
|
+
aliases: entry.aliases.flatMap(searchTerms),
|
|
362
431
|
namespace: searchTerms(entry.namespace),
|
|
363
432
|
summary: searchTerms(entry.summary),
|
|
364
433
|
tags: entry.tags.flatMap(searchTerms),
|
|
@@ -393,11 +462,21 @@ class CatalogSearchIndex {
|
|
|
393
462
|
}
|
|
394
463
|
}
|
|
395
464
|
|
|
396
|
-
/**
|
|
397
|
-
|
|
398
|
-
|
|
465
|
+
/**
|
|
466
|
+
* Methods ranked by BM25 over name, aliases, namespace, summary, tags and description, best first.
|
|
467
|
+
* The query searches for the catalog words of every vocabulary alias it says as well.
|
|
468
|
+
*/
|
|
469
|
+
public search(
|
|
470
|
+
query: string,
|
|
471
|
+
accept: (entry: IMethodEntry) => boolean,
|
|
472
|
+
): Array<{ entry: IMethodEntry; score: number; named: boolean }> {
|
|
473
|
+
const said = new Set(searchTerms(query));
|
|
474
|
+
const allTerms = [...said];
|
|
475
|
+
for (const { alias, words } of this.vocabulary) {
|
|
476
|
+
if (alias.every((term) => said.has(term))) allTerms.push(...words);
|
|
477
|
+
}
|
|
399
478
|
const meaningful = allTerms.filter((term) => !stopWords.has(term));
|
|
400
|
-
const terms = meaningful.length > 0 ? meaningful : allTerms;
|
|
479
|
+
const terms = [...new Set(meaningful.length > 0 ? meaningful : allTerms)];
|
|
401
480
|
const scores = new Map<number, number>();
|
|
402
481
|
for (const term of terms) {
|
|
403
482
|
const idf = this.idf.get(term);
|
|
@@ -407,20 +486,23 @@ class CatalogSearchIndex {
|
|
|
407
486
|
}
|
|
408
487
|
}
|
|
409
488
|
const exact = query.trim().toLowerCase();
|
|
489
|
+
const queryTerms = new Set(allTerms);
|
|
410
490
|
return [...scores.entries()]
|
|
411
491
|
.map(([index, score]) => {
|
|
412
492
|
const entry = this.entries[index];
|
|
413
|
-
const bonus = entry.path.
|
|
414
|
-
|
|
493
|
+
const bonus = [entry.path, entry.name, ...entry.aliases].some((name) => name.toLowerCase() === exact) ? 1000 : 0;
|
|
494
|
+
const named = this.nameTerms[index].some((name) => name.every((term) => queryTerms.has(term)));
|
|
495
|
+
return { entry, score: (named ? score * nameCoverageFactor : score) + bonus, named };
|
|
415
496
|
})
|
|
416
497
|
.filter(({ entry }) => accept(entry))
|
|
417
|
-
.sort((left, right) => right.score - left.score || compareCodePoints(left.entry.path, right.entry.path))
|
|
418
|
-
.map(({ entry }) => entry);
|
|
498
|
+
.sort((left, right) => right.score - left.score || compareCodePoints(left.entry.path, right.entry.path));
|
|
419
499
|
}
|
|
420
500
|
}
|
|
421
501
|
|
|
422
502
|
class FlexToolCatalogCore {
|
|
423
503
|
public readonly renderedMethods = new Map<string, IRenderedDeclaration>();
|
|
504
|
+
/** The named types each rendered method's parameters and result use. */
|
|
505
|
+
public readonly methodTypeSets = new Map<string, { input: string[]; output: string[] }>();
|
|
424
506
|
public readonly renderedTypes = new Map<string, IRenderedDeclaration>();
|
|
425
507
|
public readonly validators = new Map<string, plugins.JsonSchemaValidator>();
|
|
426
508
|
public readonly selfContainedInputs = new Map<string, TJsonSchemaNode>();
|
|
@@ -430,9 +512,12 @@ class FlexToolCatalogCore {
|
|
|
430
512
|
public readonly mode: TDeclarationMode,
|
|
431
513
|
public readonly methods: ReadonlyMap<string, IMethodEntry>,
|
|
432
514
|
public readonly types: ReadonlyMap<string, ITypeEntry>,
|
|
515
|
+
/** The identifier of each catalog type, by the name its `$ref`s use. */
|
|
516
|
+
public readonly typeIdentifiers: ReadonlyMap<string, string>,
|
|
517
|
+
vocabulary: readonly IVocabularyEntry[],
|
|
433
518
|
public readonly onProposals: IFlexToolCatalog['onProposals'] | undefined,
|
|
434
519
|
) {
|
|
435
|
-
this.index = new CatalogSearchIndex([...methods.values()]);
|
|
520
|
+
this.index = new CatalogSearchIndex([...methods.values()], vocabulary);
|
|
436
521
|
}
|
|
437
522
|
|
|
438
523
|
public static async build(catalog: unknown, mode: TDeclarationMode): Promise<FlexToolCatalogCore> {
|
|
@@ -443,19 +528,47 @@ class FlexToolCatalogCore {
|
|
|
443
528
|
if (onProposals !== undefined && typeof onProposals !== 'function') {
|
|
444
529
|
throw new FlexHarnessValidationError('A tool catalog onProposals must be a function.');
|
|
445
530
|
}
|
|
531
|
+
const vocabulary = vocabularyOf(catalog.vocabulary);
|
|
446
532
|
if (!Array.isArray(catalog.methods) || catalog.methods.length > limits.maxMethods) {
|
|
447
533
|
throw new FlexHarnessValidationError(`Tool catalog methods must be an array with at most ${limits.maxMethods} entries.`);
|
|
448
534
|
}
|
|
449
535
|
if (catalog.types !== undefined && (!Array.isArray(catalog.types) || catalog.types.length > limits.maxTypes)) {
|
|
450
536
|
throw new FlexHarnessValidationError(`Tool catalog types must be an array with at most ${limits.maxTypes} entries.`);
|
|
451
537
|
}
|
|
452
|
-
const
|
|
538
|
+
const namedTypes = new Map<string, ITypeEntry>();
|
|
453
539
|
for (const [index, type] of ((catalog.types ?? []) as unknown[]).entries()) {
|
|
454
540
|
const entry = await FlexToolCatalogCore.normalizeType(type, `Tool catalog type ${index}`);
|
|
455
|
-
if (
|
|
541
|
+
if (namedTypes.has(entry.name)) {
|
|
456
542
|
throw new FlexHarnessValidationError(`Tool catalog type "${entry.name}" is defined twice.`);
|
|
457
543
|
}
|
|
458
|
-
|
|
544
|
+
namedTypes.set(entry.name, entry);
|
|
545
|
+
}
|
|
546
|
+
// A name that is an identifier keeps it; the others are mapped in code-point order, so a
|
|
547
|
+
// catalog always declares the same identifiers.
|
|
548
|
+
const typeIdentifiers = new Map<string, string>();
|
|
549
|
+
const taken = new Set<string>();
|
|
550
|
+
const names = [...namedTypes.keys()].sort(compareCodePoints);
|
|
551
|
+
for (const name of names) {
|
|
552
|
+
if (typeIdentifierOf(name) !== name) continue;
|
|
553
|
+
typeIdentifiers.set(name, name);
|
|
554
|
+
taken.add(name);
|
|
555
|
+
}
|
|
556
|
+
for (const name of names) {
|
|
557
|
+
if (typeIdentifiers.has(name)) continue;
|
|
558
|
+
const base = typeIdentifierOf(name);
|
|
559
|
+
let identifier = base;
|
|
560
|
+
for (let suffix = 2; taken.has(identifier); suffix++) identifier = `${base}_${suffix}`;
|
|
561
|
+
typeIdentifiers.set(name, identifier);
|
|
562
|
+
taken.add(identifier);
|
|
563
|
+
}
|
|
564
|
+
/** Catalog types by the name their `$ref`s use. */
|
|
565
|
+
const catalogTypes = new Map<string, ITypeEntry>();
|
|
566
|
+
for (const [name, entry] of namedTypes) {
|
|
567
|
+
const identifier = typeIdentifiers.get(name)!;
|
|
568
|
+
if (entry.declaration !== undefined && !new RegExp(`\\b(?:interface|type|enum|class)\\s+${identifier}\\b`).test(entry.declaration)) {
|
|
569
|
+
throw new FlexHarnessValidationError(`Tool catalog type "${name}" declaration must declare "${identifier}".`);
|
|
570
|
+
}
|
|
571
|
+
catalogTypes.set(name, { ...entry, name: identifier, ...(identifier === name ? {} : { schemaName: name }) });
|
|
459
572
|
}
|
|
460
573
|
const methodInputs: Array<Omit<IMethodEntry, 'inputScope' | 'outputScope'>> = [];
|
|
461
574
|
for (const [index, method] of (catalog.methods as unknown[]).entries()) {
|
|
@@ -476,7 +589,7 @@ class FlexToolCatalogCore {
|
|
|
476
589
|
}
|
|
477
590
|
}
|
|
478
591
|
|
|
479
|
-
const registry = new Map<string, ITypeEntry>(catalogTypes);
|
|
592
|
+
const registry = new Map<string, ITypeEntry>([...catalogTypes.values()].map((type) => [type.name, type]));
|
|
480
593
|
const canonicalDefinitions = new Map<string, string>();
|
|
481
594
|
const scopeOf = (root: TJsonSchemaNode, label: string, requireSchemas: boolean): ISchemaScope => {
|
|
482
595
|
const scope: ISchemaScope = { root, localNames: new Map() };
|
|
@@ -507,7 +620,10 @@ class FlexToolCatalogCore {
|
|
|
507
620
|
};
|
|
508
621
|
for (const type of catalogTypes.values()) {
|
|
509
622
|
if (!type.schema) continue;
|
|
510
|
-
registry.set(type.name, {
|
|
623
|
+
registry.set(type.name, {
|
|
624
|
+
...type,
|
|
625
|
+
scope: scopeOf(type.schema.json, `Tool catalog type "${type.schemaName ?? type.name}"`, true),
|
|
626
|
+
});
|
|
511
627
|
}
|
|
512
628
|
const methods = new Map<string, IMethodEntry>();
|
|
513
629
|
for (const method of methodInputs) {
|
|
@@ -524,6 +640,8 @@ class FlexToolCatalogCore {
|
|
|
524
640
|
mode,
|
|
525
641
|
methods,
|
|
526
642
|
types,
|
|
643
|
+
typeIdentifiers,
|
|
644
|
+
vocabulary,
|
|
527
645
|
onProposals === undefined
|
|
528
646
|
? undefined
|
|
529
647
|
: (proposals, context) => (onProposals as NonNullable<IFlexToolCatalog['onProposals']>).call(catalog, proposals, context),
|
|
@@ -535,21 +653,16 @@ class FlexToolCatalogCore {
|
|
|
535
653
|
const unsupported = Object.keys(value).find((key) => !typeKeys.has(key));
|
|
536
654
|
if (unsupported) throw new FlexHarnessValidationError(`${label} does not support "${unsupported}".`);
|
|
537
655
|
const name = textOf(value.name, `${label} name`, { maxCharacters: limits.maxIdentifierCharacters, singleLine: true });
|
|
538
|
-
if (
|
|
539
|
-
throw new FlexHarnessValidationError(`${label} name must be an identifier that is not a reserved word.`);
|
|
540
|
-
}
|
|
656
|
+
if (/\s/.test(name)) throw new FlexHarnessValidationError(`${label} name must not contain whitespace.`);
|
|
541
657
|
if (value.schema === undefined && value.declaration === undefined) {
|
|
542
658
|
throw new FlexHarnessValidationError(`Tool catalog type "${name}" needs a schema or a declaration.`);
|
|
543
659
|
}
|
|
544
|
-
const description = value.description
|
|
545
|
-
|
|
546
|
-
|
|
660
|
+
const description = optionalTextOf(value.description, `Tool catalog type "${name}" description`, {
|
|
661
|
+
maxBytes: limits.maxDescriptionBytes,
|
|
662
|
+
})?.trim();
|
|
547
663
|
const declaration = value.declaration === undefined
|
|
548
664
|
? undefined
|
|
549
665
|
: textOf(value.declaration, `Tool catalog type "${name}" declaration`, { maxBytes: limits.maxDeclarationBytes });
|
|
550
|
-
if (declaration !== undefined && !new RegExp(`\\b(?:interface|type|enum|class)\\s+${name}\\b`).test(declaration)) {
|
|
551
|
-
throw new FlexHarnessValidationError(`Tool catalog type "${name}" declaration must declare "${name}".`);
|
|
552
|
-
}
|
|
553
666
|
const schema = value.schema === undefined
|
|
554
667
|
? undefined
|
|
555
668
|
: await resolveCatalogSchema(value.schema, `Tool catalog type "${name}" schema`);
|
|
@@ -594,12 +707,16 @@ class FlexToolCatalogCore {
|
|
|
594
707
|
const summary = textOf(value.summary, `${methodLabel} summary`, {
|
|
595
708
|
maxCharacters: limits.maxSummaryCharacters, singleLine: true,
|
|
596
709
|
}).trim();
|
|
597
|
-
const description = value.description
|
|
598
|
-
|
|
599
|
-
|
|
710
|
+
const description = optionalTextOf(value.description, `${methodLabel} description`, {
|
|
711
|
+
maxBytes: limits.maxDescriptionBytes,
|
|
712
|
+
})?.trim();
|
|
600
713
|
const tags = textListOf(value.tags, `${methodLabel} tags`, limits.maxTags, {
|
|
601
714
|
maxCharacters: limits.maxTagCharacters, singleLine: true,
|
|
602
715
|
});
|
|
716
|
+
const aliases = textListOf(value.aliases, `${methodLabel} aliases`, limits.maxAliases, {
|
|
717
|
+
maxCharacters: limits.maxAliasCharacters, singleLine: true,
|
|
718
|
+
});
|
|
719
|
+
aliases.forEach((alias, index) => namedTermsOf(alias, `${methodLabel} aliases[${index}]`));
|
|
603
720
|
const examples = textListOf(value.examples, `${methodLabel} examples`, limits.maxExamples, {
|
|
604
721
|
maxBytes: limits.maxExampleBytes,
|
|
605
722
|
});
|
|
@@ -636,6 +753,7 @@ class FlexToolCatalogCore {
|
|
|
636
753
|
summary,
|
|
637
754
|
...(description ? { description } : {}),
|
|
638
755
|
tags: Object.freeze(tags),
|
|
756
|
+
aliases: Object.freeze(aliases),
|
|
639
757
|
examples: Object.freeze(examples),
|
|
640
758
|
input,
|
|
641
759
|
...(output ? { output } : {}),
|
|
@@ -688,7 +806,7 @@ class FlexToolCatalogCore {
|
|
|
688
806
|
const local = scope.localNames.get(`${match[1]}/${key}`);
|
|
689
807
|
if (local) return local;
|
|
690
808
|
if (resolvePointer(scope.root, ref) !== undefined) return undefined;
|
|
691
|
-
return this.
|
|
809
|
+
return this.typeIdentifiers.get(key);
|
|
692
810
|
},
|
|
693
811
|
resolve: (ref: string): TJsonSchemaNode | undefined => {
|
|
694
812
|
const target = resolvePointer(scope.root, ref);
|
|
@@ -702,12 +820,17 @@ class FlexToolCatalogCore {
|
|
|
702
820
|
if (cached) return cached;
|
|
703
821
|
const type = this.types.get(name)!;
|
|
704
822
|
let rendered: IRenderedDeclaration;
|
|
823
|
+
const schemaNote = type.schemaName === undefined ? undefined : `Named "${type.schemaName}" in the schema.`;
|
|
705
824
|
if (type.declaration !== undefined) {
|
|
706
|
-
rendered = {
|
|
825
|
+
rendered = {
|
|
826
|
+
text: `${schemaNote === undefined ? '' : docBlock(schemaNote, '')}${type.declaration.trim()}`,
|
|
827
|
+
types: this.scanTypes(type.declaration, name),
|
|
828
|
+
};
|
|
707
829
|
} else {
|
|
708
830
|
const schema = type.definition ?? type.schema!.json;
|
|
709
831
|
const writer = new SchemaDeclarationWriter(this.scopeContext(type.scope!));
|
|
710
|
-
const
|
|
832
|
+
const description = [type.description, schemaNote].filter((part) => part !== undefined).join('\n\n');
|
|
833
|
+
const text = writer.namedDeclaration(name, schema, description || undefined);
|
|
711
834
|
rendered = { text, types: writer.usedTypes.filter((used) => used !== name) };
|
|
712
835
|
}
|
|
713
836
|
this.renderedTypes.set(name, rendered);
|
|
@@ -720,6 +843,9 @@ class FlexToolCatalogCore {
|
|
|
720
843
|
let rendered: IRenderedDeclaration;
|
|
721
844
|
if (entry.declaration !== undefined) {
|
|
722
845
|
rendered = { text: entry.declaration.trim(), types: this.scanTypes(entry.declaration) };
|
|
846
|
+
// A host's declaration text does not tell parameters from the result: every type it names is
|
|
847
|
+
// declared with it.
|
|
848
|
+
this.methodTypeSets.set(entry.path, { input: rendered.types, output: [] });
|
|
723
849
|
} else {
|
|
724
850
|
const inputWriter = new SchemaDeclarationWriter(this.scopeContext(entry.inputScope));
|
|
725
851
|
const members = inputWriter.objectMembers(entry.input.json);
|
|
@@ -744,6 +870,10 @@ class FlexToolCatalogCore {
|
|
|
744
870
|
text: `${docBlock(lines.join('\n'), '')}${signature}`,
|
|
745
871
|
types: [...new Set([...inputWriter.usedTypes, ...(outputWriter?.usedTypes ?? [])])].sort(compareCodePoints),
|
|
746
872
|
};
|
|
873
|
+
this.methodTypeSets.set(entry.path, {
|
|
874
|
+
input: [...new Set(inputWriter.usedTypes)].sort(compareCodePoints),
|
|
875
|
+
output: [...new Set(outputWriter?.usedTypes ?? [])].sort(compareCodePoints),
|
|
876
|
+
});
|
|
747
877
|
}
|
|
748
878
|
this.renderedMethods.set(entry.path, rendered);
|
|
749
879
|
return rendered;
|
|
@@ -757,6 +887,28 @@ class FlexToolCatalogCore {
|
|
|
757
887
|
return [...found].sort(compareCodePoints);
|
|
758
888
|
}
|
|
759
889
|
|
|
890
|
+
/** The named types a method's parameters and result use. */
|
|
891
|
+
public methodTypes(entry: IMethodEntry): { input: string[]; output: string[] } {
|
|
892
|
+
this.renderMethod(entry);
|
|
893
|
+
return this.methodTypeSets.get(entry.path)!;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** `names` and the types they use up to `depth` levels, `names` being the first, sorted. */
|
|
897
|
+
public typeLevels(names: Iterable<string>, depth: number, stop: ReadonlySet<string> = new Set()): string[] {
|
|
898
|
+
const result = new Set<string>();
|
|
899
|
+
let level = [...names];
|
|
900
|
+
for (let current = 0; current < depth && level.length > 0; current++) {
|
|
901
|
+
const next: string[] = [];
|
|
902
|
+
for (const name of level) {
|
|
903
|
+
if (result.has(name) || stop.has(name) || !this.types.has(name)) continue;
|
|
904
|
+
result.add(name);
|
|
905
|
+
next.push(...this.renderType(name).types);
|
|
906
|
+
}
|
|
907
|
+
level = next;
|
|
908
|
+
}
|
|
909
|
+
return [...result].sort(compareCodePoints);
|
|
910
|
+
}
|
|
911
|
+
|
|
760
912
|
/** `names` and every type they use, transitively, sorted. */
|
|
761
913
|
public typeClosure(names: Iterable<string>, stop: ReadonlySet<string> = new Set()): string[] {
|
|
762
914
|
const result = new Set<string>();
|
|
@@ -783,7 +935,8 @@ class FlexToolCatalogCore {
|
|
|
783
935
|
const ref = queue.pop()!;
|
|
784
936
|
const match = definitionRefPattern.exec(ref);
|
|
785
937
|
if (!match || resolvePointer(root, ref) !== undefined) continue;
|
|
786
|
-
const
|
|
938
|
+
const identifier = this.typeIdentifiers.get(decodePointerToken(match[2]));
|
|
939
|
+
const type = identifier === undefined ? undefined : this.types.get(identifier);
|
|
787
940
|
if (!type?.schema) continue;
|
|
788
941
|
const container = (isSchemaRecord(root[match[1]]) ? root[match[1]] : (root[match[1]] = {})) as Record<string, unknown>;
|
|
789
942
|
container[decodePointerToken(match[2])] = withoutDraft(structuredClone(type.schema.json) as TJsonSchemaNode);
|
|
@@ -821,6 +974,10 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
821
974
|
private readonly admitted: ReadonlySet<string> | undefined,
|
|
822
975
|
/** The limits of `run_code`; absent while code mode is off. */
|
|
823
976
|
public readonly codeMode: Readonly<IFlexCodeModeLimits> | undefined,
|
|
977
|
+
/** Levels of a result's named types a TypeScript declaration declares. */
|
|
978
|
+
private readonly outputTypeDepth: number,
|
|
979
|
+
/** `search` follows a clear top match with its declaration. */
|
|
980
|
+
private readonly describeTopMatch: boolean,
|
|
824
981
|
) {}
|
|
825
982
|
|
|
826
983
|
/**
|
|
@@ -846,7 +1003,13 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
846
1003
|
core = FlexToolCatalogCore.build(catalog, mode);
|
|
847
1004
|
byMode.set(mode, core);
|
|
848
1005
|
}
|
|
849
|
-
return new FlexToolCatalog<TScope>(
|
|
1006
|
+
return new FlexToolCatalog<TScope>(
|
|
1007
|
+
await core,
|
|
1008
|
+
undefined,
|
|
1009
|
+
normalized.codeMode,
|
|
1010
|
+
normalized.outputTypeDepth,
|
|
1011
|
+
normalized.describeTopMatch,
|
|
1012
|
+
);
|
|
850
1013
|
}
|
|
851
1014
|
|
|
852
1015
|
/** Every method path this catalog offers, sorted. */
|
|
@@ -866,6 +1029,8 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
866
1029
|
this.core,
|
|
867
1030
|
new Set([...this.core.methods.keys()].filter((path) => allowed.has(path) && this.isAdmitted(path))),
|
|
868
1031
|
this.codeMode,
|
|
1032
|
+
this.outputTypeDepth,
|
|
1033
|
+
this.describeTopMatch,
|
|
869
1034
|
);
|
|
870
1035
|
}
|
|
871
1036
|
|
|
@@ -887,6 +1052,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
887
1052
|
'name, summary and description; `namespace` (a dotted path) narrows the search or, without `query`, lists',
|
|
888
1053
|
`the namespace like ls. Read a method's ${typescript ? 'TypeScript declaration' : 'description and JSON Schema'}`,
|
|
889
1054
|
'with describe_tools before calling it.',
|
|
1055
|
+
...(this.describeTopMatch ? ['When one match clearly leads, its declaration follows the results.'] : []),
|
|
890
1056
|
this.topLevelLine(),
|
|
891
1057
|
].join(' '),
|
|
892
1058
|
inputSchema: {
|
|
@@ -967,8 +1133,14 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
967
1133
|
return await this.core.onProposals(proposals, context as IFlexCatalogProposalContext);
|
|
968
1134
|
}
|
|
969
1135
|
|
|
970
|
-
/**
|
|
971
|
-
|
|
1136
|
+
/**
|
|
1137
|
+
* Search results or a namespace listing as text. A clear top match of a query's first page is
|
|
1138
|
+
* followed by its declaration, as `describe` gives it with `options`.
|
|
1139
|
+
*/
|
|
1140
|
+
public search(
|
|
1141
|
+
input: IFlexCatalogSearchInput = {},
|
|
1142
|
+
options: { declaredTypes?: Iterable<string>; maxBytes?: number } = {},
|
|
1143
|
+
): string {
|
|
972
1144
|
const limit = input.limit ?? limits.defaultSearchResults;
|
|
973
1145
|
const offset = input.offset ?? 0;
|
|
974
1146
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > limits.maxSearchResults) {
|
|
@@ -994,7 +1166,16 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
994
1166
|
if (matches.length === 0) {
|
|
995
1167
|
return [`No methods match "${query}"${namespace ? ` in ${namespace}` : ''}.`, this.topLevelLine()].join('\n');
|
|
996
1168
|
}
|
|
997
|
-
|
|
1169
|
+
const results = this.page(matches.map(({ entry }) => `${entry.path} - ${entry.summary}`), offset, limit, 'matches');
|
|
1170
|
+
const [top, next] = matches;
|
|
1171
|
+
const clear = offset === 0 && (!next || (top.named && top.score >= clearMatchMargin * next.score));
|
|
1172
|
+
if (!this.describeTopMatch || !clear) return results;
|
|
1173
|
+
// The declaration follows only while the whole answer fits what describe_tools may answer;
|
|
1174
|
+
// an answer over the output limit would lose the results too.
|
|
1175
|
+
const maxBytes = Math.min(options.maxBytes ?? limits.maxDescribeBytes, limits.maxDescribeBytes);
|
|
1176
|
+
const declaration = this.describe({ methods: [top.entry.path] }, { ...options, maxBytes }).output;
|
|
1177
|
+
const answer = `${results}\n\n${typeof declaration === 'string' ? declaration : JSON.stringify(declaration)}`;
|
|
1178
|
+
return Buffer.byteLength(JSON.stringify(answer), 'utf8') <= maxBytes ? answer : results;
|
|
998
1179
|
}
|
|
999
1180
|
const node = tree.get(namespace)!;
|
|
1000
1181
|
const lines = [...node.children.entries()].map(([child, count]) => `${child}.* (${count} method${count === 1 ? '' : 's'})`);
|
|
@@ -1022,8 +1203,11 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1022
1203
|
const entries = requestedMethods.filter((path) => this.has(path)).sort(compareCodePoints)
|
|
1023
1204
|
.map((path) => this.core.methods.get(path)!);
|
|
1024
1205
|
const unknownMethods = requestedMethods.filter((path) => !this.has(path)).sort(compareCodePoints);
|
|
1025
|
-
|
|
1026
|
-
const
|
|
1206
|
+
// A type is asked for by its identifier or by the name its schema gives it.
|
|
1207
|
+
const identifierOf = (name: string): string | undefined =>
|
|
1208
|
+
this.core.types.has(name) ? name : this.core.typeIdentifiers.get(name);
|
|
1209
|
+
const knownTypes = [...new Set(requestedTypes.flatMap((name) => identifierOf(name) ?? []))].sort(compareCodePoints);
|
|
1210
|
+
const unknownTypes = requestedTypes.filter((name) => identifierOf(name) === undefined).sort(compareCodePoints);
|
|
1027
1211
|
const notes = [
|
|
1028
1212
|
...unknownMethods.map((path) => {
|
|
1029
1213
|
const suggestions = closeMatches(path, this.methods);
|
|
@@ -1042,18 +1226,18 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1042
1226
|
: this.describeTypescript(entries, knownTypes, notes, new Set(options.declaredTypes ?? []), maxBytes);
|
|
1043
1227
|
}
|
|
1044
1228
|
|
|
1045
|
-
/** The named types `describe_tools` results in `messages` declared. */
|
|
1229
|
+
/** The named types the `describe_tools` and `search_tools` results in `messages` declared. */
|
|
1046
1230
|
public declaredTypesIn(messages: readonly plugins.ModelMessage[] | undefined): Set<string> {
|
|
1047
1231
|
const declared = new Set<string>();
|
|
1048
1232
|
for (const message of messages ?? []) {
|
|
1049
1233
|
if (message.role !== 'tool' || !Array.isArray(message.content)) continue;
|
|
1050
1234
|
for (const part of message.content) {
|
|
1051
|
-
if (part.type !== 'tool-result' || part.toolName !== toolNames.describe) continue;
|
|
1235
|
+
if (part.type !== 'tool-result' || (part.toolName !== toolNames.describe && part.toolName !== toolNames.search)) continue;
|
|
1052
1236
|
const output = part.output as { type?: string; value?: unknown };
|
|
1053
|
-
if (output.type !== 'text' || typeof output.value !== 'string'
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1237
|
+
if (output.type !== 'text' || typeof output.value !== 'string') continue;
|
|
1238
|
+
for (const [, header] of output.value.matchAll(declaredTypesLine)) {
|
|
1239
|
+
for (const name of header.split(', ')) if (this.core.types.has(name)) declared.add(name);
|
|
1240
|
+
}
|
|
1057
1241
|
}
|
|
1058
1242
|
}
|
|
1059
1243
|
return declared;
|
|
@@ -1196,10 +1380,24 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1196
1380
|
const included: IMethodEntry[] = [];
|
|
1197
1381
|
const omitted: string[] = [];
|
|
1198
1382
|
const alreadyDeclared = new Set([...declared].filter((name) => !requestedTypes.includes(name)));
|
|
1199
|
-
|
|
1383
|
+
// Parameters' types are declared in full; a result's only `outputTypeDepth` levels deep.
|
|
1384
|
+
const typesFor = (methods: IMethodEntry[]): string[] => {
|
|
1385
|
+
const inputs = [...requestedTypes, ...methods.flatMap((entry) => this.core.methodTypes(entry).input)];
|
|
1386
|
+
const outputs = methods.flatMap((entry) => this.core.methodTypes(entry).output);
|
|
1387
|
+
return [...new Set([
|
|
1388
|
+
...this.core.typeClosure(inputs, alreadyDeclared),
|
|
1389
|
+
...this.core.typeLevels(outputs, this.outputTypeDepth, alreadyDeclared),
|
|
1390
|
+
])].sort(compareCodePoints);
|
|
1391
|
+
};
|
|
1392
|
+
let types = typesFor([]);
|
|
1200
1393
|
const assemble = (methods: IMethodEntry[], typeNames: string[], skipped: string[]): string => {
|
|
1201
|
-
const
|
|
1202
|
-
const
|
|
1394
|
+
const shown = new Set(typeNames);
|
|
1395
|
+
const named = new Set([
|
|
1396
|
+
...methods.flatMap((entry) => this.core.renderMethod(entry).types),
|
|
1397
|
+
...typeNames.flatMap((name) => this.core.renderType(name).types),
|
|
1398
|
+
]);
|
|
1399
|
+
const earlier = [...named].filter((name) => declared.has(name) && !shown.has(name)).sort(compareCodePoints);
|
|
1400
|
+
const undeclared = [...named].filter((name) => !declared.has(name) && !shown.has(name)).sort(compareCodePoints);
|
|
1203
1401
|
const blocks: string[] = [];
|
|
1204
1402
|
if (typeNames.length) blocks.push(`${declaredTypesHeader}${typeNames.join(', ')}`);
|
|
1205
1403
|
for (const name of typeNames) blocks.push(this.core.renderType(name).text);
|
|
@@ -1212,6 +1410,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1212
1410
|
}
|
|
1213
1411
|
const trailing = [
|
|
1214
1412
|
...(earlier.length ? [`Declared earlier: ${earlier.join(', ')}; describe_tools({"types":[...]}) declares them again.`] : []),
|
|
1413
|
+
...(undeclared.length ? [`Not declared yet: ${undeclared.join(', ')}; describe_tools({"types":[...]}) declares them.`] : []),
|
|
1215
1414
|
...notes,
|
|
1216
1415
|
...(skipped.length ? [`Not included to stay within the size limit: ${skipped.join(', ')}; describe them in another call.`] : []),
|
|
1217
1416
|
];
|
|
@@ -1220,10 +1419,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1220
1419
|
};
|
|
1221
1420
|
for (const entry of entries) {
|
|
1222
1421
|
const candidate = [...included, entry];
|
|
1223
|
-
const candidateTypes =
|
|
1224
|
-
[...requestedTypes, ...candidate.flatMap((method) => this.core.renderMethod(method).types)],
|
|
1225
|
-
alreadyDeclared,
|
|
1226
|
-
);
|
|
1422
|
+
const candidateTypes = typesFor(candidate);
|
|
1227
1423
|
const text = assemble(candidate, candidateTypes, entries.slice(candidate.length).map((method) => method.path));
|
|
1228
1424
|
if (Buffer.byteLength(JSON.stringify(text), 'utf8') > maxBytes && included.length > 0) {
|
|
1229
1425
|
omitted.push(...entries.slice(included.length).map((method) => method.path));
|
|
@@ -1248,7 +1444,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1248
1444
|
const type = this.core.types.get(name)!;
|
|
1249
1445
|
const schema = type.definition ?? type.schema?.json;
|
|
1250
1446
|
return [{
|
|
1251
|
-
name,
|
|
1447
|
+
name: type.schemaName ?? name,
|
|
1252
1448
|
...(type.description ? { description: type.description } : {}),
|
|
1253
1449
|
...(schema === undefined ? { declaration: type.declaration! } : { schema: withoutDraft(schema) }),
|
|
1254
1450
|
}];
|
|
@@ -1291,7 +1487,8 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1291
1487
|
namedType: (ref) => {
|
|
1292
1488
|
const match = definitionRefPattern.exec(ref);
|
|
1293
1489
|
const key = match ? decodePointerToken(match[2]) : undefined;
|
|
1294
|
-
|
|
1490
|
+
if (key === undefined) return undefined;
|
|
1491
|
+
return this.core.typeIdentifiers.get(key) ?? (typeNamePattern.test(key) ? key : undefined);
|
|
1295
1492
|
},
|
|
1296
1493
|
resolve: (ref) => {
|
|
1297
1494
|
const target = resolvePointer(root, ref);
|