@modelprofile.com/flexharness 10.13.0 → 10.15.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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.codesandbox.d.ts +39 -0
- package/dist_ts/classes.codesandbox.js +539 -0
- package/dist_ts/classes.flexharness.d.ts +10 -0
- package/dist_ts/classes.flexharness.js +148 -16
- package/dist_ts/classes.toolcatalog.d.ts +27 -7
- package/dist_ts/classes.toolcatalog.js +311 -69
- package/dist_ts/interfaces.d.ts +137 -5
- package/dist_ts/interfaces.js +32 -1
- package/dist_ts/plugins.d.ts +62 -1
- package/dist_ts/plugins.js +15 -1
- package/dist_ts/utils.harnessoptions.d.ts +9 -2
- package/dist_ts/utils.harnessoptions.js +52 -11
- package/dist_ts/utils.json.js +3 -1
- package/dist_ts/utils.tooloutput.d.ts +2 -0
- package/dist_ts/utils.tooloutput.js +11 -1
- package/dist_ts_compaction/plugins.d.ts +3 -3
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.d.ts +10 -1
- package/dist_ts_compaction/smartagent.compaction.js +79 -38
- package/package.json +16 -4
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +565 -0
- package/ts/classes.flexharness.ts +152 -15
- package/ts/classes.toolcatalog.ts +344 -66
- package/ts/interfaces.ts +169 -5
- package/ts/plugins.ts +79 -0
- package/ts/readme.md +1 -1
- package/ts/utils.harnessoptions.ts +68 -12
- package/ts/utils.json.ts +2 -0
- package/ts/utils.tooloutput.ts +9 -0
- package/ts_compaction/plugins.ts +3 -3
- package/ts_compaction/smartagent.compaction.ts +90 -34
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { FlexHarnessValidationError } from './errors.js';
|
|
3
3
|
import {
|
|
4
|
+
FLEX_CODE_MODE_TOOL_NAME,
|
|
4
5
|
FLEX_TOOL_CATALOG_LIMITS as limits,
|
|
5
6
|
FLEX_TOOL_CATALOG_TOOL_NAMES as toolNames,
|
|
7
|
+
type IFlexCatalogProposal,
|
|
8
|
+
type IFlexCatalogProposalContext,
|
|
9
|
+
type IFlexCodeModeLimits,
|
|
10
|
+
type TFlexCatalogCallDecision,
|
|
6
11
|
type IFlexCatalogDescribeInput,
|
|
7
12
|
type IFlexCatalogDescribeResult,
|
|
8
13
|
type IFlexCatalogMethod,
|
|
@@ -25,6 +30,7 @@ import {
|
|
|
25
30
|
type IRenderedDeclaration,
|
|
26
31
|
type TJsonSchemaNode,
|
|
27
32
|
} from './utils.schemadeclarations.js';
|
|
33
|
+
import { normalizeToolCatalogOptions } from './utils.harnessoptions.js';
|
|
28
34
|
import { validateUtf8String } from './utils.validation.js';
|
|
29
35
|
|
|
30
36
|
type TValidationResult = { success: true; value: unknown } | { success: false; error: Error };
|
|
@@ -51,6 +57,7 @@ interface IMethodEntry {
|
|
|
51
57
|
readonly summary: string;
|
|
52
58
|
readonly description?: string;
|
|
53
59
|
readonly tags: readonly string[];
|
|
60
|
+
readonly aliases: readonly string[];
|
|
54
61
|
readonly examples: readonly string[];
|
|
55
62
|
readonly input: ICatalogSchema;
|
|
56
63
|
readonly inputScope: ISchemaScope;
|
|
@@ -58,10 +65,14 @@ interface IMethodEntry {
|
|
|
58
65
|
readonly outputScope?: ISchemaScope;
|
|
59
66
|
readonly declaration?: string;
|
|
60
67
|
readonly run: IFlexCatalogMethod['run'];
|
|
68
|
+
readonly onCall?: NonNullable<IFlexCatalogMethod['onCall']>;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
interface ITypeEntry {
|
|
72
|
+
/** The identifier the type is declared under. */
|
|
64
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;
|
|
65
76
|
readonly description?: string;
|
|
66
77
|
readonly declaration?: string;
|
|
67
78
|
/** A catalog type's schema. */
|
|
@@ -76,6 +87,8 @@ interface IPreparedCall {
|
|
|
76
87
|
readonly success: true;
|
|
77
88
|
readonly method: string;
|
|
78
89
|
readonly params: unknown;
|
|
90
|
+
/** The method's `onCall` decision for the call; `run` without the hook. */
|
|
91
|
+
decide(context: IFlexCatalogRunContext<any>): Promise<TFlexCatalogCallDecision>;
|
|
79
92
|
run(context: IFlexCatalogRunContext<any>): ReturnType<IFlexCatalogMethod['run']>;
|
|
80
93
|
}
|
|
81
94
|
|
|
@@ -98,17 +111,28 @@ const reservedWords = new Set([
|
|
|
98
111
|
const controlPattern = /[\u0000-\u001f\u007f]/;
|
|
99
112
|
const definitionRefPattern = /^#\/(\$defs|definitions)\/([^/]+)$/;
|
|
100
113
|
const declaredTypesHeader = '// Declares types: ';
|
|
114
|
+
const declaredTypesLine = /^\/\/ Declares types: (.*)$/gm;
|
|
101
115
|
const identifierScanPattern = /[A-Za-z_$][A-Za-z0-9_$]*/g;
|
|
102
|
-
const describeFormats = new Set(['typescript', 'json-schema']);
|
|
103
116
|
const methodKeys = new Set([
|
|
104
|
-
'method', 'summary', 'description', 'tags', 'examples', 'inputSchema', 'outputSchema', 'declaration', 'run',
|
|
117
|
+
'method', 'summary', 'description', 'tags', 'aliases', 'examples', 'inputSchema', 'outputSchema', 'declaration', 'run', 'onCall',
|
|
105
118
|
]);
|
|
119
|
+
const catalogKeys = new Set(['methods', 'types', 'vocabulary', 'onProposals']);
|
|
106
120
|
const typeKeys = new Set(['name', 'description', 'schema', 'declaration']);
|
|
107
121
|
const stopWords = new Set([
|
|
108
|
-
'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',
|
|
109
123
|
'of', 'on', 'or', 'that', 'the', 'this', 'to', 'with',
|
|
110
124
|
]);
|
|
111
|
-
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;
|
|
112
136
|
type TSearchField = keyof typeof fieldWeights;
|
|
113
137
|
const bm25K1 = 1.2;
|
|
114
138
|
const bm25B = 0.75;
|
|
@@ -207,6 +231,13 @@ const searchTerms = (text: string): string[] => text
|
|
|
207
231
|
.filter(Boolean)
|
|
208
232
|
.map(stem);
|
|
209
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
|
+
|
|
210
241
|
const editDistance = (left: string, right: string, limit: number): number => {
|
|
211
242
|
if (Math.abs(left.length - right.length) > limit) return limit + 1;
|
|
212
243
|
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
@@ -292,6 +323,25 @@ const textOf = (
|
|
|
292
323
|
return value;
|
|
293
324
|
};
|
|
294
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
|
+
|
|
295
345
|
const textListOf = (
|
|
296
346
|
value: unknown,
|
|
297
347
|
label: string,
|
|
@@ -305,6 +355,30 @@ const textListOf = (
|
|
|
305
355
|
return value.map((entry, index) => textOf(entry, `${label}[${index}]`, options));
|
|
306
356
|
};
|
|
307
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
|
+
|
|
308
382
|
async function resolveCatalogSchema(schema: unknown, label: string): Promise<ICatalogSchema> {
|
|
309
383
|
let json: unknown;
|
|
310
384
|
let validate: TValidate | undefined;
|
|
@@ -346,10 +420,14 @@ async function resolveCatalogSchema(schema: unknown, label: string): Promise<ICa
|
|
|
346
420
|
class CatalogSearchIndex {
|
|
347
421
|
private readonly postings = new Map<string, Array<{ index: number; weight: number }>>();
|
|
348
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[]>>;
|
|
349
425
|
|
|
350
|
-
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))]));
|
|
351
428
|
const fieldTexts = (entry: IMethodEntry): Record<TSearchField, string[]> => ({
|
|
352
429
|
name: searchTerms(entry.name),
|
|
430
|
+
aliases: entry.aliases.flatMap(searchTerms),
|
|
353
431
|
namespace: searchTerms(entry.namespace),
|
|
354
432
|
summary: searchTerms(entry.summary),
|
|
355
433
|
tags: entry.tags.flatMap(searchTerms),
|
|
@@ -384,11 +462,21 @@ class CatalogSearchIndex {
|
|
|
384
462
|
}
|
|
385
463
|
}
|
|
386
464
|
|
|
387
|
-
/**
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
}
|
|
390
478
|
const meaningful = allTerms.filter((term) => !stopWords.has(term));
|
|
391
|
-
const terms = meaningful.length > 0 ? meaningful : allTerms;
|
|
479
|
+
const terms = [...new Set(meaningful.length > 0 ? meaningful : allTerms)];
|
|
392
480
|
const scores = new Map<number, number>();
|
|
393
481
|
for (const term of terms) {
|
|
394
482
|
const idf = this.idf.get(term);
|
|
@@ -398,20 +486,23 @@ class CatalogSearchIndex {
|
|
|
398
486
|
}
|
|
399
487
|
}
|
|
400
488
|
const exact = query.trim().toLowerCase();
|
|
489
|
+
const queryTerms = new Set(allTerms);
|
|
401
490
|
return [...scores.entries()]
|
|
402
491
|
.map(([index, score]) => {
|
|
403
492
|
const entry = this.entries[index];
|
|
404
|
-
const bonus = entry.path.
|
|
405
|
-
|
|
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 };
|
|
406
496
|
})
|
|
407
497
|
.filter(({ entry }) => accept(entry))
|
|
408
|
-
.sort((left, right) => right.score - left.score || compareCodePoints(left.entry.path, right.entry.path))
|
|
409
|
-
.map(({ entry }) => entry);
|
|
498
|
+
.sort((left, right) => right.score - left.score || compareCodePoints(left.entry.path, right.entry.path));
|
|
410
499
|
}
|
|
411
500
|
}
|
|
412
501
|
|
|
413
502
|
class FlexToolCatalogCore {
|
|
414
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[] }>();
|
|
415
506
|
public readonly renderedTypes = new Map<string, IRenderedDeclaration>();
|
|
416
507
|
public readonly validators = new Map<string, plugins.JsonSchemaValidator>();
|
|
417
508
|
public readonly selfContainedInputs = new Map<string, TJsonSchemaNode>();
|
|
@@ -421,27 +512,63 @@ class FlexToolCatalogCore {
|
|
|
421
512
|
public readonly mode: TDeclarationMode,
|
|
422
513
|
public readonly methods: ReadonlyMap<string, IMethodEntry>,
|
|
423
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[],
|
|
518
|
+
public readonly onProposals: IFlexToolCatalog['onProposals'] | undefined,
|
|
424
519
|
) {
|
|
425
|
-
this.index = new CatalogSearchIndex([...methods.values()]);
|
|
520
|
+
this.index = new CatalogSearchIndex([...methods.values()], vocabulary);
|
|
426
521
|
}
|
|
427
522
|
|
|
428
523
|
public static async build(catalog: unknown, mode: TDeclarationMode): Promise<FlexToolCatalogCore> {
|
|
429
524
|
if (!isPlainObject(catalog)) throw new FlexHarnessValidationError('A tool catalog must be a plain object.');
|
|
430
|
-
const unsupported = Object.keys(catalog).find((key) => key
|
|
525
|
+
const unsupported = Object.keys(catalog).find((key) => !catalogKeys.has(key));
|
|
431
526
|
if (unsupported) throw new FlexHarnessValidationError(`A tool catalog does not support "${unsupported}".`);
|
|
527
|
+
const onProposals = catalog.onProposals;
|
|
528
|
+
if (onProposals !== undefined && typeof onProposals !== 'function') {
|
|
529
|
+
throw new FlexHarnessValidationError('A tool catalog onProposals must be a function.');
|
|
530
|
+
}
|
|
531
|
+
const vocabulary = vocabularyOf(catalog.vocabulary);
|
|
432
532
|
if (!Array.isArray(catalog.methods) || catalog.methods.length > limits.maxMethods) {
|
|
433
533
|
throw new FlexHarnessValidationError(`Tool catalog methods must be an array with at most ${limits.maxMethods} entries.`);
|
|
434
534
|
}
|
|
435
535
|
if (catalog.types !== undefined && (!Array.isArray(catalog.types) || catalog.types.length > limits.maxTypes)) {
|
|
436
536
|
throw new FlexHarnessValidationError(`Tool catalog types must be an array with at most ${limits.maxTypes} entries.`);
|
|
437
537
|
}
|
|
438
|
-
const
|
|
538
|
+
const namedTypes = new Map<string, ITypeEntry>();
|
|
439
539
|
for (const [index, type] of ((catalog.types ?? []) as unknown[]).entries()) {
|
|
440
540
|
const entry = await FlexToolCatalogCore.normalizeType(type, `Tool catalog type ${index}`);
|
|
441
|
-
if (
|
|
541
|
+
if (namedTypes.has(entry.name)) {
|
|
442
542
|
throw new FlexHarnessValidationError(`Tool catalog type "${entry.name}" is defined twice.`);
|
|
443
543
|
}
|
|
444
|
-
|
|
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 }) });
|
|
445
572
|
}
|
|
446
573
|
const methodInputs: Array<Omit<IMethodEntry, 'inputScope' | 'outputScope'>> = [];
|
|
447
574
|
for (const [index, method] of (catalog.methods as unknown[]).entries()) {
|
|
@@ -462,7 +589,7 @@ class FlexToolCatalogCore {
|
|
|
462
589
|
}
|
|
463
590
|
}
|
|
464
591
|
|
|
465
|
-
const registry = new Map<string, ITypeEntry>(catalogTypes);
|
|
592
|
+
const registry = new Map<string, ITypeEntry>([...catalogTypes.values()].map((type) => [type.name, type]));
|
|
466
593
|
const canonicalDefinitions = new Map<string, string>();
|
|
467
594
|
const scopeOf = (root: TJsonSchemaNode, label: string, requireSchemas: boolean): ISchemaScope => {
|
|
468
595
|
const scope: ISchemaScope = { root, localNames: new Map() };
|
|
@@ -493,7 +620,10 @@ class FlexToolCatalogCore {
|
|
|
493
620
|
};
|
|
494
621
|
for (const type of catalogTypes.values()) {
|
|
495
622
|
if (!type.schema) continue;
|
|
496
|
-
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
|
+
});
|
|
497
627
|
}
|
|
498
628
|
const methods = new Map<string, IMethodEntry>();
|
|
499
629
|
for (const method of methodInputs) {
|
|
@@ -506,7 +636,16 @@ class FlexToolCatalogCore {
|
|
|
506
636
|
});
|
|
507
637
|
}
|
|
508
638
|
const types = new Map([...registry.entries()].sort(([left], [right]) => compareCodePoints(left, right)));
|
|
509
|
-
return new FlexToolCatalogCore(
|
|
639
|
+
return new FlexToolCatalogCore(
|
|
640
|
+
mode,
|
|
641
|
+
methods,
|
|
642
|
+
types,
|
|
643
|
+
typeIdentifiers,
|
|
644
|
+
vocabulary,
|
|
645
|
+
onProposals === undefined
|
|
646
|
+
? undefined
|
|
647
|
+
: (proposals, context) => (onProposals as NonNullable<IFlexToolCatalog['onProposals']>).call(catalog, proposals, context),
|
|
648
|
+
);
|
|
510
649
|
}
|
|
511
650
|
|
|
512
651
|
private static async normalizeType(value: unknown, label: string): Promise<ITypeEntry> {
|
|
@@ -514,21 +653,16 @@ class FlexToolCatalogCore {
|
|
|
514
653
|
const unsupported = Object.keys(value).find((key) => !typeKeys.has(key));
|
|
515
654
|
if (unsupported) throw new FlexHarnessValidationError(`${label} does not support "${unsupported}".`);
|
|
516
655
|
const name = textOf(value.name, `${label} name`, { maxCharacters: limits.maxIdentifierCharacters, singleLine: true });
|
|
517
|
-
if (
|
|
518
|
-
throw new FlexHarnessValidationError(`${label} name must be an identifier that is not a reserved word.`);
|
|
519
|
-
}
|
|
656
|
+
if (/\s/.test(name)) throw new FlexHarnessValidationError(`${label} name must not contain whitespace.`);
|
|
520
657
|
if (value.schema === undefined && value.declaration === undefined) {
|
|
521
658
|
throw new FlexHarnessValidationError(`Tool catalog type "${name}" needs a schema or a declaration.`);
|
|
522
659
|
}
|
|
523
|
-
const description = value.description
|
|
524
|
-
|
|
525
|
-
|
|
660
|
+
const description = optionalTextOf(value.description, `Tool catalog type "${name}" description`, {
|
|
661
|
+
maxBytes: limits.maxDescriptionBytes,
|
|
662
|
+
})?.trim();
|
|
526
663
|
const declaration = value.declaration === undefined
|
|
527
664
|
? undefined
|
|
528
665
|
: textOf(value.declaration, `Tool catalog type "${name}" declaration`, { maxBytes: limits.maxDeclarationBytes });
|
|
529
|
-
if (declaration !== undefined && !new RegExp(`\\b(?:interface|type|enum|class)\\s+${name}\\b`).test(declaration)) {
|
|
530
|
-
throw new FlexHarnessValidationError(`Tool catalog type "${name}" declaration must declare "${name}".`);
|
|
531
|
-
}
|
|
532
666
|
const schema = value.schema === undefined
|
|
533
667
|
? undefined
|
|
534
668
|
: await resolveCatalogSchema(value.schema, `Tool catalog type "${name}" schema`);
|
|
@@ -573,12 +707,16 @@ class FlexToolCatalogCore {
|
|
|
573
707
|
const summary = textOf(value.summary, `${methodLabel} summary`, {
|
|
574
708
|
maxCharacters: limits.maxSummaryCharacters, singleLine: true,
|
|
575
709
|
}).trim();
|
|
576
|
-
const description = value.description
|
|
577
|
-
|
|
578
|
-
|
|
710
|
+
const description = optionalTextOf(value.description, `${methodLabel} description`, {
|
|
711
|
+
maxBytes: limits.maxDescriptionBytes,
|
|
712
|
+
})?.trim();
|
|
579
713
|
const tags = textListOf(value.tags, `${methodLabel} tags`, limits.maxTags, {
|
|
580
714
|
maxCharacters: limits.maxTagCharacters, singleLine: true,
|
|
581
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}]`));
|
|
582
720
|
const examples = textListOf(value.examples, `${methodLabel} examples`, limits.maxExamples, {
|
|
583
721
|
maxBytes: limits.maxExampleBytes,
|
|
584
722
|
});
|
|
@@ -590,6 +728,9 @@ class FlexToolCatalogCore {
|
|
|
590
728
|
throw new FlexHarnessValidationError(`${methodLabel} declaration must declare "function ${name}(".`);
|
|
591
729
|
}
|
|
592
730
|
if (typeof value.run !== 'function') throw new FlexHarnessValidationError(`${methodLabel} run must be a function.`);
|
|
731
|
+
if (value.onCall !== undefined && typeof value.onCall !== 'function') {
|
|
732
|
+
throw new FlexHarnessValidationError(`${methodLabel} onCall must be a function.`);
|
|
733
|
+
}
|
|
593
734
|
if (value.inputSchema === undefined) throw new FlexHarnessValidationError(`${methodLabel} needs an inputSchema.`);
|
|
594
735
|
const input = await resolveCatalogSchema(value.inputSchema, `${methodLabel} inputSchema`);
|
|
595
736
|
const inputJson = input.json;
|
|
@@ -604,6 +745,7 @@ class FlexToolCatalogCore {
|
|
|
604
745
|
? undefined
|
|
605
746
|
: await resolveCatalogSchema(value.outputSchema, `${methodLabel} outputSchema`);
|
|
606
747
|
const run = value.run as IFlexCatalogMethod['run'];
|
|
748
|
+
const onCall = value.onCall as IFlexCatalogMethod['onCall'];
|
|
607
749
|
return {
|
|
608
750
|
path,
|
|
609
751
|
namespace: segments.slice(0, -1).join('.'),
|
|
@@ -611,11 +753,15 @@ class FlexToolCatalogCore {
|
|
|
611
753
|
summary,
|
|
612
754
|
...(description ? { description } : {}),
|
|
613
755
|
tags: Object.freeze(tags),
|
|
756
|
+
aliases: Object.freeze(aliases),
|
|
614
757
|
examples: Object.freeze(examples),
|
|
615
758
|
input,
|
|
616
759
|
...(output ? { output } : {}),
|
|
617
760
|
...(declaration === undefined ? {} : { declaration }),
|
|
618
761
|
run: (params, context) => run.call(value, params, context),
|
|
762
|
+
...(onCall === undefined ? {} : {
|
|
763
|
+
onCall: (params: unknown, context: IFlexCatalogRunContext) => onCall.call(value, params, context),
|
|
764
|
+
}),
|
|
619
765
|
};
|
|
620
766
|
}
|
|
621
767
|
|
|
@@ -660,7 +806,7 @@ class FlexToolCatalogCore {
|
|
|
660
806
|
const local = scope.localNames.get(`${match[1]}/${key}`);
|
|
661
807
|
if (local) return local;
|
|
662
808
|
if (resolvePointer(scope.root, ref) !== undefined) return undefined;
|
|
663
|
-
return this.
|
|
809
|
+
return this.typeIdentifiers.get(key);
|
|
664
810
|
},
|
|
665
811
|
resolve: (ref: string): TJsonSchemaNode | undefined => {
|
|
666
812
|
const target = resolvePointer(scope.root, ref);
|
|
@@ -674,12 +820,17 @@ class FlexToolCatalogCore {
|
|
|
674
820
|
if (cached) return cached;
|
|
675
821
|
const type = this.types.get(name)!;
|
|
676
822
|
let rendered: IRenderedDeclaration;
|
|
823
|
+
const schemaNote = type.schemaName === undefined ? undefined : `Named "${type.schemaName}" in the schema.`;
|
|
677
824
|
if (type.declaration !== undefined) {
|
|
678
|
-
rendered = {
|
|
825
|
+
rendered = {
|
|
826
|
+
text: `${schemaNote === undefined ? '' : docBlock(schemaNote, '')}${type.declaration.trim()}`,
|
|
827
|
+
types: this.scanTypes(type.declaration, name),
|
|
828
|
+
};
|
|
679
829
|
} else {
|
|
680
830
|
const schema = type.definition ?? type.schema!.json;
|
|
681
831
|
const writer = new SchemaDeclarationWriter(this.scopeContext(type.scope!));
|
|
682
|
-
const
|
|
832
|
+
const description = [type.description, schemaNote].filter((part) => part !== undefined).join('\n\n');
|
|
833
|
+
const text = writer.namedDeclaration(name, schema, description || undefined);
|
|
683
834
|
rendered = { text, types: writer.usedTypes.filter((used) => used !== name) };
|
|
684
835
|
}
|
|
685
836
|
this.renderedTypes.set(name, rendered);
|
|
@@ -692,6 +843,9 @@ class FlexToolCatalogCore {
|
|
|
692
843
|
let rendered: IRenderedDeclaration;
|
|
693
844
|
if (entry.declaration !== undefined) {
|
|
694
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: [] });
|
|
695
849
|
} else {
|
|
696
850
|
const inputWriter = new SchemaDeclarationWriter(this.scopeContext(entry.inputScope));
|
|
697
851
|
const members = inputWriter.objectMembers(entry.input.json);
|
|
@@ -716,6 +870,10 @@ class FlexToolCatalogCore {
|
|
|
716
870
|
text: `${docBlock(lines.join('\n'), '')}${signature}`,
|
|
717
871
|
types: [...new Set([...inputWriter.usedTypes, ...(outputWriter?.usedTypes ?? [])])].sort(compareCodePoints),
|
|
718
872
|
};
|
|
873
|
+
this.methodTypeSets.set(entry.path, {
|
|
874
|
+
input: [...new Set(inputWriter.usedTypes)].sort(compareCodePoints),
|
|
875
|
+
output: [...new Set(outputWriter?.usedTypes ?? [])].sort(compareCodePoints),
|
|
876
|
+
});
|
|
719
877
|
}
|
|
720
878
|
this.renderedMethods.set(entry.path, rendered);
|
|
721
879
|
return rendered;
|
|
@@ -729,6 +887,28 @@ class FlexToolCatalogCore {
|
|
|
729
887
|
return [...found].sort(compareCodePoints);
|
|
730
888
|
}
|
|
731
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
|
+
|
|
732
912
|
/** `names` and every type they use, transitively, sorted. */
|
|
733
913
|
public typeClosure(names: Iterable<string>, stop: ReadonlySet<string> = new Set()): string[] {
|
|
734
914
|
const result = new Set<string>();
|
|
@@ -755,7 +935,8 @@ class FlexToolCatalogCore {
|
|
|
755
935
|
const ref = queue.pop()!;
|
|
756
936
|
const match = definitionRefPattern.exec(ref);
|
|
757
937
|
if (!match || resolvePointer(root, ref) !== undefined) continue;
|
|
758
|
-
const
|
|
938
|
+
const identifier = this.typeIdentifiers.get(decodePointerToken(match[2]));
|
|
939
|
+
const type = identifier === undefined ? undefined : this.types.get(identifier);
|
|
759
940
|
if (!type?.schema) continue;
|
|
760
941
|
const container = (isSchemaRecord(root[match[1]]) ? root[match[1]] : (root[match[1]] = {})) as Record<string, unknown>;
|
|
761
942
|
container[decodePointerToken(match[2])] = withoutDraft(structuredClone(type.schema.json) as TJsonSchemaNode);
|
|
@@ -791,6 +972,12 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
791
972
|
private constructor(
|
|
792
973
|
private readonly core: FlexToolCatalogCore,
|
|
793
974
|
private readonly admitted: ReadonlySet<string> | undefined,
|
|
975
|
+
/** The limits of `run_code`; absent while code mode is off. */
|
|
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,
|
|
794
981
|
) {}
|
|
795
982
|
|
|
796
983
|
/**
|
|
@@ -801,10 +988,8 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
801
988
|
catalog: IFlexToolCatalog<TScope>,
|
|
802
989
|
options: IFlexToolCatalogOptions = {},
|
|
803
990
|
): Promise<FlexToolCatalog<TScope>> {
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
throw new FlexHarnessValidationError('toolCatalog.declarations must be "typescript" or "json-schema".');
|
|
807
|
-
}
|
|
991
|
+
const normalized = normalizeToolCatalogOptions(options);
|
|
992
|
+
const mode = normalized.declarations;
|
|
808
993
|
if (typeof catalog !== 'object' || catalog === null) {
|
|
809
994
|
throw new FlexHarnessValidationError('A tool catalog must be a plain object.');
|
|
810
995
|
}
|
|
@@ -818,7 +1003,13 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
818
1003
|
core = FlexToolCatalogCore.build(catalog, mode);
|
|
819
1004
|
byMode.set(mode, core);
|
|
820
1005
|
}
|
|
821
|
-
return new FlexToolCatalog<TScope>(
|
|
1006
|
+
return new FlexToolCatalog<TScope>(
|
|
1007
|
+
await core,
|
|
1008
|
+
undefined,
|
|
1009
|
+
normalized.codeMode,
|
|
1010
|
+
normalized.outputTypeDepth,
|
|
1011
|
+
normalized.describeTopMatch,
|
|
1012
|
+
);
|
|
822
1013
|
}
|
|
823
1014
|
|
|
824
1015
|
/** Every method path this catalog offers, sorted. */
|
|
@@ -837,6 +1028,9 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
837
1028
|
return new FlexToolCatalog<TScope>(
|
|
838
1029
|
this.core,
|
|
839
1030
|
new Set([...this.core.methods.keys()].filter((path) => allowed.has(path) && this.isAdmitted(path))),
|
|
1031
|
+
this.codeMode,
|
|
1032
|
+
this.outputTypeDepth,
|
|
1033
|
+
this.describeTopMatch,
|
|
840
1034
|
);
|
|
841
1035
|
}
|
|
842
1036
|
|
|
@@ -844,7 +1038,10 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
844
1038
|
return this.core.methods.has(path) && this.isAdmitted(path);
|
|
845
1039
|
}
|
|
846
1040
|
|
|
847
|
-
/**
|
|
1041
|
+
/**
|
|
1042
|
+
* The built-in tools as the model sees them: the three catalog tools, and `run_code` in code
|
|
1043
|
+
* mode. Their text depends only on the catalog and its options.
|
|
1044
|
+
*/
|
|
848
1045
|
public toolDefinitions(): Record<string, IFlexCatalogToolDefinition> {
|
|
849
1046
|
const typescript = this.core.mode === 'typescript';
|
|
850
1047
|
const methodPath = { type: 'string', maxLength: limits.maxMethodPathCharacters } as const;
|
|
@@ -855,6 +1052,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
855
1052
|
'name, summary and description; `namespace` (a dotted path) narrows the search or, without `query`, lists',
|
|
856
1053
|
`the namespace like ls. Read a method's ${typescript ? 'TypeScript declaration' : 'description and JSON Schema'}`,
|
|
857
1054
|
'with describe_tools before calling it.',
|
|
1055
|
+
...(this.describeTopMatch ? ['When one match clearly leads, its declaration follows the results.'] : []),
|
|
858
1056
|
this.topLevelLine(),
|
|
859
1057
|
].join(' '),
|
|
860
1058
|
inputSchema: {
|
|
@@ -899,11 +1097,50 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
899
1097
|
additionalProperties: false,
|
|
900
1098
|
},
|
|
901
1099
|
},
|
|
1100
|
+
...(this.codeMode ? { [FLEX_CODE_MODE_TOOL_NAME]: this.codeToolDefinition(this.codeMode) } : {}),
|
|
902
1101
|
};
|
|
903
1102
|
}
|
|
904
1103
|
|
|
905
|
-
|
|
906
|
-
|
|
1104
|
+
private codeToolDefinition(codeMode: Readonly<IFlexCodeModeLimits>): IFlexCatalogToolDefinition {
|
|
1105
|
+
return {
|
|
1106
|
+
description: [
|
|
1107
|
+
'Run a short TypeScript or JavaScript program against the catalog, for work that takes several calls:',
|
|
1108
|
+
'loops, filters, joins. Call a method as `await api.<dotted path>(params)`, for example',
|
|
1109
|
+
'`await api.banking.listMovements({ accountId })`, with the declarations describe_tools returns; a refused',
|
|
1110
|
+
'call throws an `ApiError` whose `details` say why. `return` a JSON value: it and the `console.log` output',
|
|
1111
|
+
'come back, nothing else does. No network, files, timers or imports;',
|
|
1112
|
+
`at most ${codeMode.maxCalls} calls and ${Math.round(codeMode.wallMs / 1000)} seconds.`,
|
|
1113
|
+
].join(' '),
|
|
1114
|
+
inputSchema: {
|
|
1115
|
+
type: 'object',
|
|
1116
|
+
properties: { code: { type: 'string', minLength: 1, maxLength: codeMode.maxSourceBytes } },
|
|
1117
|
+
required: ['code'],
|
|
1118
|
+
additionalProperties: false,
|
|
1119
|
+
},
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/** Hands proposed calls to the catalog's `onProposals` and returns what the model sees for them. */
|
|
1124
|
+
public async propose(
|
|
1125
|
+
proposals: readonly IFlexCatalogProposal[],
|
|
1126
|
+
context: IFlexCatalogProposalContext<TScope>,
|
|
1127
|
+
): Promise<unknown> {
|
|
1128
|
+
if (!this.core.onProposals) {
|
|
1129
|
+
throw new FlexHarnessValidationError(
|
|
1130
|
+
`Tool catalog method "${proposals[0]?.method}" proposed a call, but the catalog has no onProposals.`,
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
return await this.core.onProposals(proposals, context as IFlexCatalogProposalContext);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
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 {
|
|
907
1144
|
const limit = input.limit ?? limits.defaultSearchResults;
|
|
908
1145
|
const offset = input.offset ?? 0;
|
|
909
1146
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > limits.maxSearchResults) {
|
|
@@ -929,7 +1166,16 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
929
1166
|
if (matches.length === 0) {
|
|
930
1167
|
return [`No methods match "${query}"${namespace ? ` in ${namespace}` : ''}.`, this.topLevelLine()].join('\n');
|
|
931
1168
|
}
|
|
932
|
-
|
|
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;
|
|
933
1179
|
}
|
|
934
1180
|
const node = tree.get(namespace)!;
|
|
935
1181
|
const lines = [...node.children.entries()].map(([child, count]) => `${child}.* (${count} method${count === 1 ? '' : 's'})`);
|
|
@@ -957,8 +1203,11 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
957
1203
|
const entries = requestedMethods.filter((path) => this.has(path)).sort(compareCodePoints)
|
|
958
1204
|
.map((path) => this.core.methods.get(path)!);
|
|
959
1205
|
const unknownMethods = requestedMethods.filter((path) => !this.has(path)).sort(compareCodePoints);
|
|
960
|
-
|
|
961
|
-
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);
|
|
962
1211
|
const notes = [
|
|
963
1212
|
...unknownMethods.map((path) => {
|
|
964
1213
|
const suggestions = closeMatches(path, this.methods);
|
|
@@ -977,18 +1226,18 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
977
1226
|
: this.describeTypescript(entries, knownTypes, notes, new Set(options.declaredTypes ?? []), maxBytes);
|
|
978
1227
|
}
|
|
979
1228
|
|
|
980
|
-
/** The named types `describe_tools` results in `messages` declared. */
|
|
1229
|
+
/** The named types the `describe_tools` and `search_tools` results in `messages` declared. */
|
|
981
1230
|
public declaredTypesIn(messages: readonly plugins.ModelMessage[] | undefined): Set<string> {
|
|
982
1231
|
const declared = new Set<string>();
|
|
983
1232
|
for (const message of messages ?? []) {
|
|
984
1233
|
if (message.role !== 'tool' || !Array.isArray(message.content)) continue;
|
|
985
1234
|
for (const part of message.content) {
|
|
986
|
-
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;
|
|
987
1236
|
const output = part.output as { type?: string; value?: unknown };
|
|
988
|
-
if (output.type !== 'text' || typeof output.value !== 'string'
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
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
|
+
}
|
|
992
1241
|
}
|
|
993
1242
|
}
|
|
994
1243
|
return declared;
|
|
@@ -1041,6 +1290,19 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1041
1290
|
success: true,
|
|
1042
1291
|
method: path,
|
|
1043
1292
|
params: validated,
|
|
1293
|
+
decide: async (context) => {
|
|
1294
|
+
const decision = entry.onCall ? await entry.onCall(validated, context) : 'run';
|
|
1295
|
+
if (decision === 'run' || decision === 'propose') return decision;
|
|
1296
|
+
if (
|
|
1297
|
+
isSchemaRecord(decision)
|
|
1298
|
+
&& Object.keys(decision).length === 1
|
|
1299
|
+
&& typeof decision.deny === 'string'
|
|
1300
|
+
&& decision.deny.trim()
|
|
1301
|
+
) {
|
|
1302
|
+
return { deny: decision.deny.slice(0, 2000) };
|
|
1303
|
+
}
|
|
1304
|
+
throw new FlexHarnessValidationError(`Tool catalog method "${path}" onCall returned an invalid decision.`);
|
|
1305
|
+
},
|
|
1044
1306
|
run: (context) => entry.run(validated, context),
|
|
1045
1307
|
};
|
|
1046
1308
|
}
|
|
@@ -1118,10 +1380,24 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1118
1380
|
const included: IMethodEntry[] = [];
|
|
1119
1381
|
const omitted: string[] = [];
|
|
1120
1382
|
const alreadyDeclared = new Set([...declared].filter((name) => !requestedTypes.includes(name)));
|
|
1121
|
-
|
|
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([]);
|
|
1122
1393
|
const assemble = (methods: IMethodEntry[], typeNames: string[], skipped: string[]): string => {
|
|
1123
|
-
const
|
|
1124
|
-
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);
|
|
1125
1401
|
const blocks: string[] = [];
|
|
1126
1402
|
if (typeNames.length) blocks.push(`${declaredTypesHeader}${typeNames.join(', ')}`);
|
|
1127
1403
|
for (const name of typeNames) blocks.push(this.core.renderType(name).text);
|
|
@@ -1134,6 +1410,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1134
1410
|
}
|
|
1135
1411
|
const trailing = [
|
|
1136
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.`] : []),
|
|
1137
1414
|
...notes,
|
|
1138
1415
|
...(skipped.length ? [`Not included to stay within the size limit: ${skipped.join(', ')}; describe them in another call.`] : []),
|
|
1139
1416
|
];
|
|
@@ -1142,10 +1419,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1142
1419
|
};
|
|
1143
1420
|
for (const entry of entries) {
|
|
1144
1421
|
const candidate = [...included, entry];
|
|
1145
|
-
const candidateTypes =
|
|
1146
|
-
[...requestedTypes, ...candidate.flatMap((method) => this.core.renderMethod(method).types)],
|
|
1147
|
-
alreadyDeclared,
|
|
1148
|
-
);
|
|
1422
|
+
const candidateTypes = typesFor(candidate);
|
|
1149
1423
|
const text = assemble(candidate, candidateTypes, entries.slice(candidate.length).map((method) => method.path));
|
|
1150
1424
|
if (Buffer.byteLength(JSON.stringify(text), 'utf8') > maxBytes && included.length > 0) {
|
|
1151
1425
|
omitted.push(...entries.slice(included.length).map((method) => method.path));
|
|
@@ -1170,7 +1444,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1170
1444
|
const type = this.core.types.get(name)!;
|
|
1171
1445
|
const schema = type.definition ?? type.schema?.json;
|
|
1172
1446
|
return [{
|
|
1173
|
-
name,
|
|
1447
|
+
name: type.schemaName ?? name,
|
|
1174
1448
|
...(type.description ? { description: type.description } : {}),
|
|
1175
1449
|
...(schema === undefined ? { declaration: type.declaration! } : { schema: withoutDraft(schema) }),
|
|
1176
1450
|
}];
|
|
@@ -1213,7 +1487,8 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1213
1487
|
namedType: (ref) => {
|
|
1214
1488
|
const match = definitionRefPattern.exec(ref);
|
|
1215
1489
|
const key = match ? decodePointerToken(match[2]) : undefined;
|
|
1216
|
-
|
|
1490
|
+
if (key === undefined) return undefined;
|
|
1491
|
+
return this.core.typeIdentifiers.get(key) ?? (typeNamePattern.test(key) ? key : undefined);
|
|
1217
1492
|
},
|
|
1218
1493
|
resolve: (ref) => {
|
|
1219
1494
|
const target = resolvePointer(root, ref);
|
|
@@ -1383,14 +1658,17 @@ const constraintText = (subset: Record<string, unknown>): string => {
|
|
|
1383
1658
|
};
|
|
1384
1659
|
|
|
1385
1660
|
/**
|
|
1386
|
-
* While a run has a catalog, its
|
|
1387
|
-
* every tool name.
|
|
1661
|
+
* While a run has a catalog, its tool names are reserved (`run_code` in code mode too), and a
|
|
1662
|
+
* method path must differ from every tool name.
|
|
1388
1663
|
*/
|
|
1389
1664
|
export const assertNoCatalogCollisions = (
|
|
1390
1665
|
catalog: FlexToolCatalog<unknown>,
|
|
1391
1666
|
tools: Readonly<Record<string, unknown>>,
|
|
1392
1667
|
): void => {
|
|
1393
|
-
const catalogToolNames = new Set<string>(
|
|
1668
|
+
const catalogToolNames = new Set<string>([
|
|
1669
|
+
...Object.values(toolNames),
|
|
1670
|
+
...(catalog.codeMode ? [FLEX_CODE_MODE_TOOL_NAME] : []),
|
|
1671
|
+
]);
|
|
1394
1672
|
for (const name of catalogToolNames) {
|
|
1395
1673
|
if (Object.prototype.hasOwnProperty.call(tools, name)) {
|
|
1396
1674
|
throw new FlexHarnessValidationError(`Tool provider cannot define enabled built-in tool "${name}".`);
|