@modelprofile.com/flexharness 10.15.1 → 10.17.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.js +6 -2
- package/dist_ts/classes.flexharness.d.ts +36 -0
- package/dist_ts/classes.flexharness.js +360 -56
- package/dist_ts/classes.stores.d.ts +3 -1
- package/dist_ts/classes.stores.js +92 -2
- package/dist_ts/classes.toolcatalog.d.ts +18 -1
- package/dist_ts/classes.toolcatalog.js +179 -18
- package/dist_ts/interfaces.d.ts +67 -0
- package/dist_ts/interfaces.js +12 -2
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.json.d.ts +5 -0
- package/dist_ts/utils.json.js +35 -1
- package/dist_ts/utils.results.d.ts +21 -0
- package/dist_ts/utils.results.js +158 -0
- package/dist_ts/utils.storerules.d.ts +5 -0
- package/dist_ts/utils.storerules.js +10 -1
- package/dist_ts/utils.tooloutput.d.ts +5 -2
- package/dist_ts/utils.tooloutput.js +10 -3
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +2 -1
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +63 -5
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +6 -3
- package/dist_ts_stores_nosqldb/classes.recordstore.js +8 -2
- package/dist_ts_typedrequest/catalog.d.ts +28 -0
- package/dist_ts_typedrequest/catalog.js +79 -0
- package/dist_ts_typedrequest/describe.d.ts +10 -0
- package/dist_ts_typedrequest/describe.js +189 -0
- package/dist_ts_typedrequest/index.d.ts +3 -0
- package/dist_ts_typedrequest/index.js +4 -0
- package/dist_ts_typedrequest/interfaces.d.ts +107 -0
- package/dist_ts_typedrequest/interfaces.js +2 -0
- package/dist_ts_typedrequest/plugins.d.ts +7 -0
- package/dist_ts_typedrequest/plugins.js +11 -0
- package/package.json +14 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +5 -1
- package/ts/classes.flexharness.ts +419 -55
- package/ts/classes.stores.ts +109 -0
- package/ts/classes.toolcatalog.ts +177 -14
- package/ts/interfaces.ts +69 -1
- package/ts/plugins.ts +3 -0
- package/ts/utils.json.ts +30 -0
- package/ts/utils.results.ts +164 -0
- package/ts/utils.storerules.ts +15 -0
- package/ts/utils.tooloutput.ts +9 -2
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +68 -2
- package/ts_stores_nosqldb/classes.recordstore.ts +14 -4
- package/ts_typedrequest/catalog.ts +91 -0
- package/ts_typedrequest/describe.ts +211 -0
- package/ts_typedrequest/index.ts +3 -0
- package/ts_typedrequest/interfaces.ts +124 -0
- package/ts_typedrequest/plugins.ts +15 -0
- package/ts_typedrequest/readme.md +66 -0
package/ts/classes.stores.ts
CHANGED
|
@@ -16,9 +16,11 @@ import type {
|
|
|
16
16
|
IFlexProjectManagementWriteContext,
|
|
17
17
|
IFlexProjectionSnapshotV3,
|
|
18
18
|
IFlexProjectionStore,
|
|
19
|
+
IFlexResultStore,
|
|
19
20
|
IFlexScopeSnapshot,
|
|
20
21
|
IFlexScopeStore,
|
|
21
22
|
IFlexToolJobStoreProvider,
|
|
23
|
+
TJsonValue,
|
|
22
24
|
TFlexProjectionSnapshot,
|
|
23
25
|
TFlexProjectManagementRecord,
|
|
24
26
|
} from './interfaces.js';
|
|
@@ -50,6 +52,7 @@ import {
|
|
|
50
52
|
providerKey,
|
|
51
53
|
validateExpectedRevision,
|
|
52
54
|
validateIdentifier,
|
|
55
|
+
validateResultIndex,
|
|
53
56
|
validateProjectManagementWriteContext,
|
|
54
57
|
validateSnapshotSave,
|
|
55
58
|
} from './utils.storerules.js';
|
|
@@ -336,6 +339,36 @@ class InMemoryToolJobStoreProvider implements IFlexToolJobStoreProvider {
|
|
|
336
339
|
}
|
|
337
340
|
}
|
|
338
341
|
|
|
342
|
+
class InMemoryResultStore implements IFlexResultStore {
|
|
343
|
+
private readonly sessions = new Map<string, Map<string, TJsonValue>>();
|
|
344
|
+
|
|
345
|
+
public async save(storageKey: string, sessionId: string, handle: string, value: TJsonValue): Promise<void> {
|
|
346
|
+
validateIdentifier(handle, 'handle');
|
|
347
|
+
const key = providerKey(storageKey, sessionId);
|
|
348
|
+
const results = this.sessions.get(key) ?? new Map<string, TJsonValue>();
|
|
349
|
+
if (results.has(handle)) throw new FlexHarnessStoreConflictError(`${key}:${handle}`, 0, 1);
|
|
350
|
+
results.set(handle, cloneSerializable(value));
|
|
351
|
+
this.sessions.set(key, results);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
public async load(storageKey: string, sessionId: string, handle: string): Promise<TJsonValue | undefined> {
|
|
355
|
+
const value = this.sessions.get(providerKey(storageKey, sessionId))?.get(handle);
|
|
356
|
+
return value === undefined ? undefined : cloneSerializable(value);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public async list(storageKey: string, sessionId: string): Promise<string[]> {
|
|
360
|
+
return [...this.sessions.get(providerKey(storageKey, sessionId))?.keys() ?? []];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
public async delete(storageKey: string, sessionId: string, handle: string): Promise<void> {
|
|
364
|
+
this.sessions.get(providerKey(storageKey, sessionId))?.delete(handle);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
public async deleteSession(storageKey: string, sessionId: string): Promise<void> {
|
|
368
|
+
this.sessions.delete(providerKey(storageKey, sessionId));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
339
372
|
export class InMemoryFlexHarnessStores implements IFlexHarnessStores {
|
|
340
373
|
public readonly scopes: IFlexScopeStore = new InMemoryScopeStore();
|
|
341
374
|
public readonly projections: IFlexProjectionStore = new InMemoryProjectionStore();
|
|
@@ -343,6 +376,7 @@ export class InMemoryFlexHarnessStores implements IFlexHarnessStores {
|
|
|
343
376
|
public readonly agentEvents: IFlexAgentEventStoreProvider = new InMemoryAgentEventStoreProvider();
|
|
344
377
|
public readonly jobs: IFlexToolJobStoreProvider = new InMemoryToolJobStoreProvider();
|
|
345
378
|
public readonly projectManagement = new InMemoryFlexProjectManagementStore();
|
|
379
|
+
public readonly results: IFlexResultStore = new InMemoryResultStore();
|
|
346
380
|
}
|
|
347
381
|
|
|
348
382
|
const jsonDomains = [
|
|
@@ -353,6 +387,7 @@ const jsonDomains = [
|
|
|
353
387
|
'events',
|
|
354
388
|
'archives',
|
|
355
389
|
'jobs',
|
|
390
|
+
'results',
|
|
356
391
|
] as const;
|
|
357
392
|
type TJsonDomain = (typeof jsonDomains)[number];
|
|
358
393
|
|
|
@@ -433,6 +468,21 @@ class JsonFileRoot {
|
|
|
433
468
|
);
|
|
434
469
|
}
|
|
435
470
|
|
|
471
|
+
public resultPath(storageKey: string, sessionId: string, handle: string): string {
|
|
472
|
+
validateIdentifier(handle, 'handle');
|
|
473
|
+
return plugins.path.join(this.resultSessionPath(storageKey, sessionId), `${this.digest(handle)}.json`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
public resultIndexPath(storageKey: string, sessionId: string): string {
|
|
477
|
+
return plugins.path.join(this.resultSessionPath(storageKey, sessionId), 'index.json');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
public resultSessionPath(storageKey: string, sessionId: string): string {
|
|
481
|
+
validateIdentifier(storageKey, 'storageKey');
|
|
482
|
+
validateIdentifier(sessionId, 'sessionId');
|
|
483
|
+
return plugins.path.join(this.directory, 'results', this.digest(storageKey), this.digest(sessionId));
|
|
484
|
+
}
|
|
485
|
+
|
|
436
486
|
public eventQueueKey(storageKey: string, sessionId: string): string {
|
|
437
487
|
return `event:${this.sessionPath('events', storageKey, sessionId)}`;
|
|
438
488
|
}
|
|
@@ -851,6 +901,63 @@ class JsonPermissionStore implements IFlexPermissionStore {
|
|
|
851
901
|
}
|
|
852
902
|
}
|
|
853
903
|
|
|
904
|
+
/**
|
|
905
|
+
* One file per result and an index of the session's handles, oldest first. Every operation of a
|
|
906
|
+
* session runs in the session's queue, so the index follows the files.
|
|
907
|
+
*/
|
|
908
|
+
class JsonResultStore implements IFlexResultStore {
|
|
909
|
+
constructor(private readonly root: JsonFileRoot) {}
|
|
910
|
+
|
|
911
|
+
private inSessionQueue<T>(storageKey: string, sessionId: string, operation: () => Promise<T>): Promise<T> {
|
|
912
|
+
return JsonFileRoot.inQueue(`results:${this.root.resultSessionPath(storageKey, sessionId)}`, operation);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
private readIndex(storageKey: string, sessionId: string): Promise<string[]> {
|
|
916
|
+
return this.root.read(this.root.resultIndexPath(storageKey, sessionId), validateResultIndex)
|
|
917
|
+
.then((index) => index?.handles ?? []);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
public save(storageKey: string, sessionId: string, handle: string, value: TJsonValue): Promise<void> {
|
|
921
|
+
const filePath = this.root.resultPath(storageKey, sessionId, handle);
|
|
922
|
+
return this.inSessionQueue(storageKey, sessionId, async () => {
|
|
923
|
+
if (await this.root.read(filePath, (stored) => stored) !== undefined) {
|
|
924
|
+
throw new FlexHarnessStoreConflictError(`${providerKey(storageKey, sessionId)}:${handle}`, 0, 1);
|
|
925
|
+
}
|
|
926
|
+
await this.root.write(filePath, value);
|
|
927
|
+
const handles = await this.readIndex(storageKey, sessionId);
|
|
928
|
+
await this.root.write(this.root.resultIndexPath(storageKey, sessionId), { handles: [...handles, handle] });
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
public load(storageKey: string, sessionId: string, handle: string): Promise<TJsonValue | undefined> {
|
|
933
|
+
const filePath = this.root.resultPath(storageKey, sessionId, handle);
|
|
934
|
+
return this.inSessionQueue(storageKey, sessionId, () => this.root.read(filePath, (value) => {
|
|
935
|
+
assertJsonSerializable(value);
|
|
936
|
+
return value as TJsonValue;
|
|
937
|
+
}));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
public list(storageKey: string, sessionId: string): Promise<string[]> {
|
|
941
|
+
return this.inSessionQueue(storageKey, sessionId, () => this.readIndex(storageKey, sessionId));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
public delete(storageKey: string, sessionId: string, handle: string): Promise<void> {
|
|
945
|
+
return this.inSessionQueue(storageKey, sessionId, async () => {
|
|
946
|
+
const handles = await this.readIndex(storageKey, sessionId);
|
|
947
|
+
await this.root.write(
|
|
948
|
+
this.root.resultIndexPath(storageKey, sessionId),
|
|
949
|
+
{ handles: handles.filter((entry) => entry !== handle) },
|
|
950
|
+
);
|
|
951
|
+
await this.root.deleteFile(this.root.resultPath(storageKey, sessionId, handle));
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
public deleteSession(storageKey: string, sessionId: string): Promise<void> {
|
|
956
|
+
const directory = this.root.resultSessionPath(storageKey, sessionId);
|
|
957
|
+
return this.inSessionQueue(storageKey, sessionId, () => this.root.deleteDirectory(directory));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
854
961
|
class JsonProjectManagementStore implements IFlexProjectManagementStore {
|
|
855
962
|
constructor(private readonly root: JsonFileRoot) {}
|
|
856
963
|
|
|
@@ -1186,6 +1293,7 @@ export class JsonFileFlexHarnessStores implements IFlexHarnessStores {
|
|
|
1186
1293
|
public readonly agentEvents: IFlexAgentEventStoreProvider;
|
|
1187
1294
|
public readonly jobs: IFlexToolJobStoreProvider;
|
|
1188
1295
|
public readonly projectManagement: IFlexProjectManagementStore;
|
|
1296
|
+
public readonly results: IFlexResultStore;
|
|
1189
1297
|
private readonly root: JsonFileRoot;
|
|
1190
1298
|
|
|
1191
1299
|
constructor(options: IJsonFileFlexHarnessStoresOptions) {
|
|
@@ -1199,6 +1307,7 @@ export class JsonFileFlexHarnessStores implements IFlexHarnessStores {
|
|
|
1199
1307
|
this.projectManagement = new JsonProjectManagementStore(this.root);
|
|
1200
1308
|
this.agentEvents = new JsonAgentEventStoreProvider(this.root);
|
|
1201
1309
|
this.jobs = new JsonToolJobStoreProvider(this.root);
|
|
1310
|
+
this.results = new JsonResultStore(this.root);
|
|
1202
1311
|
}
|
|
1203
1312
|
|
|
1204
1313
|
public dispose(): Promise<void> {
|
|
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import { FlexHarnessValidationError } from './errors.js';
|
|
3
3
|
import {
|
|
4
4
|
FLEX_CODE_MODE_TOOL_NAME,
|
|
5
|
+
FLEX_RESULT_METHOD,
|
|
5
6
|
FLEX_TOOL_CATALOG_LIMITS as limits,
|
|
6
7
|
FLEX_TOOL_CATALOG_TOOL_NAMES as toolNames,
|
|
7
8
|
type IFlexCatalogProposal,
|
|
@@ -66,6 +67,7 @@ interface IMethodEntry {
|
|
|
66
67
|
readonly declaration?: string;
|
|
67
68
|
readonly run: IFlexCatalogMethod['run'];
|
|
68
69
|
readonly onCall?: NonNullable<IFlexCatalogMethod['onCall']>;
|
|
70
|
+
readonly readOnly: boolean;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
interface ITypeEntry {
|
|
@@ -115,6 +117,7 @@ const declaredTypesLine = /^\/\/ Declares types: (.*)$/gm;
|
|
|
115
117
|
const identifierScanPattern = /[A-Za-z_$][A-Za-z0-9_$]*/g;
|
|
116
118
|
const methodKeys = new Set([
|
|
117
119
|
'method', 'summary', 'description', 'tags', 'aliases', 'examples', 'inputSchema', 'outputSchema', 'declaration', 'run', 'onCall',
|
|
120
|
+
'readOnly',
|
|
118
121
|
]);
|
|
119
122
|
const catalogKeys = new Set(['methods', 'types', 'vocabulary', 'onProposals']);
|
|
120
123
|
const typeKeys = new Set(['name', 'description', 'schema', 'declaration']);
|
|
@@ -136,7 +139,58 @@ const clearMatchMargin = 1.25;
|
|
|
136
139
|
type TSearchField = keyof typeof fieldWeights;
|
|
137
140
|
const bm25K1 = 1.2;
|
|
138
141
|
const bm25B = 0.75;
|
|
139
|
-
const coreCache = new WeakMap<object, Map<
|
|
142
|
+
const coreCache = new WeakMap<object, Map<string, Promise<FlexToolCatalogCore>>>();
|
|
143
|
+
|
|
144
|
+
/** The catalog with `results.get` added; a catalog of its own cannot define it. */
|
|
145
|
+
function withResultsMethod(catalog: IFlexToolCatalog<any>): IFlexToolCatalog<any> {
|
|
146
|
+
const methods = Array.isArray(catalog.methods) ? catalog.methods : [];
|
|
147
|
+
if (methods.some((method) => isPlainObject(method) && method.method === FLEX_RESULT_METHOD)) {
|
|
148
|
+
throw new FlexHarnessValidationError(`Tool catalog method "${FLEX_RESULT_METHOD}" is reserved for stored results.`);
|
|
149
|
+
}
|
|
150
|
+
return { ...catalog, methods: [...methods, resultsMethod] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* `results.get`, which reads a stored result. FlexHarness serves its calls itself; the entry gives
|
|
155
|
+
* it a declaration and parameter validation like any method. Search and namespace listings leave it
|
|
156
|
+
* out: the result handle names it.
|
|
157
|
+
*/
|
|
158
|
+
const resultsMethod: IFlexCatalogMethod = {
|
|
159
|
+
method: FLEX_RESULT_METHOD,
|
|
160
|
+
summary: 'Read a page of a tool result that was too large to return whole.',
|
|
161
|
+
description: 'A result too large for the output budget comes back as { $flexType: "result", handle, summary, pages }. '
|
|
162
|
+
+ 'This reads one page of it, or of the value at `path`, a dotted path such as "items.3.lines". Each page holds '
|
|
163
|
+
+ 'whole array items, object entries or a slice of a string; a part too large for a page stands as '
|
|
164
|
+
+ '{ $flexType: "oversize", path } to read by its path.',
|
|
165
|
+
readOnly: true,
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
handle: { type: 'string', minLength: 1, maxLength: 64, description: 'The handle of the stored result.' },
|
|
170
|
+
page: { type: 'integer', minimum: 1, description: 'The page to read, from 1. Defaults to 1.' },
|
|
171
|
+
path: { type: 'string', maxLength: 512, description: 'A dotted path into the result. Defaults to the whole result.' },
|
|
172
|
+
},
|
|
173
|
+
required: ['handle'],
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
},
|
|
176
|
+
outputSchema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
handle: { type: 'string' },
|
|
180
|
+
path: { type: 'string' },
|
|
181
|
+
page: { type: 'integer' },
|
|
182
|
+
pages: { type: 'integer' },
|
|
183
|
+
items: { type: 'array', items: { type: 'integer' }, description: 'The array items the page holds, [from, to).' },
|
|
184
|
+
entries: { type: 'array', items: { type: 'integer' }, description: 'The object entries the page holds, [from, to).' },
|
|
185
|
+
chars: { type: 'array', items: { type: 'integer' }, description: 'The string characters the page holds, [from, to).' },
|
|
186
|
+
value: {},
|
|
187
|
+
},
|
|
188
|
+
required: ['handle', 'path', 'page', 'pages', 'value'],
|
|
189
|
+
},
|
|
190
|
+
run: () => {
|
|
191
|
+
throw new FlexHarnessValidationError(`${FLEX_RESULT_METHOD} is served by the session that stored the result.`);
|
|
192
|
+
},
|
|
193
|
+
};
|
|
140
194
|
|
|
141
195
|
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
142
196
|
typeof value === 'object'
|
|
@@ -516,11 +570,17 @@ class FlexToolCatalogCore {
|
|
|
516
570
|
public readonly typeIdentifiers: ReadonlyMap<string, string>,
|
|
517
571
|
vocabulary: readonly IVocabularyEntry[],
|
|
518
572
|
public readonly onProposals: IFlexToolCatalog['onProposals'] | undefined,
|
|
573
|
+
/** Built-in methods that search and namespace listings leave out. */
|
|
574
|
+
public readonly unlisted: ReadonlySet<string> = new Set(),
|
|
519
575
|
) {
|
|
520
|
-
this.index = new CatalogSearchIndex([...methods.values()], vocabulary);
|
|
576
|
+
this.index = new CatalogSearchIndex([...methods.values()].filter((entry) => !unlisted.has(entry.path)), vocabulary);
|
|
521
577
|
}
|
|
522
578
|
|
|
523
|
-
public static async build(
|
|
579
|
+
public static async build(
|
|
580
|
+
catalog: unknown,
|
|
581
|
+
mode: TDeclarationMode,
|
|
582
|
+
unlisted: ReadonlySet<string> = new Set(),
|
|
583
|
+
): Promise<FlexToolCatalogCore> {
|
|
524
584
|
if (!isPlainObject(catalog)) throw new FlexHarnessValidationError('A tool catalog must be a plain object.');
|
|
525
585
|
const unsupported = Object.keys(catalog).find((key) => !catalogKeys.has(key));
|
|
526
586
|
if (unsupported) throw new FlexHarnessValidationError(`A tool catalog does not support "${unsupported}".`);
|
|
@@ -645,6 +705,7 @@ class FlexToolCatalogCore {
|
|
|
645
705
|
onProposals === undefined
|
|
646
706
|
? undefined
|
|
647
707
|
: (proposals, context) => (onProposals as NonNullable<IFlexToolCatalog['onProposals']>).call(catalog, proposals, context),
|
|
708
|
+
unlisted,
|
|
648
709
|
);
|
|
649
710
|
}
|
|
650
711
|
|
|
@@ -731,6 +792,9 @@ class FlexToolCatalogCore {
|
|
|
731
792
|
if (value.onCall !== undefined && typeof value.onCall !== 'function') {
|
|
732
793
|
throw new FlexHarnessValidationError(`${methodLabel} onCall must be a function.`);
|
|
733
794
|
}
|
|
795
|
+
if (value.readOnly !== undefined && typeof value.readOnly !== 'boolean') {
|
|
796
|
+
throw new FlexHarnessValidationError(`${methodLabel} readOnly must be a boolean.`);
|
|
797
|
+
}
|
|
734
798
|
if (value.inputSchema === undefined) throw new FlexHarnessValidationError(`${methodLabel} needs an inputSchema.`);
|
|
735
799
|
const input = await resolveCatalogSchema(value.inputSchema, `${methodLabel} inputSchema`);
|
|
736
800
|
const inputJson = input.json;
|
|
@@ -762,6 +826,7 @@ class FlexToolCatalogCore {
|
|
|
762
826
|
...(onCall === undefined ? {} : {
|
|
763
827
|
onCall: (params: unknown, context: IFlexCatalogRunContext) => onCall.call(value, params, context),
|
|
764
828
|
}),
|
|
829
|
+
readOnly: value.readOnly === true,
|
|
765
830
|
};
|
|
766
831
|
}
|
|
767
832
|
|
|
@@ -842,10 +907,13 @@ class FlexToolCatalogCore {
|
|
|
842
907
|
if (cached) return cached;
|
|
843
908
|
let rendered: IRenderedDeclaration;
|
|
844
909
|
if (entry.declaration !== undefined) {
|
|
845
|
-
|
|
846
|
-
//
|
|
847
|
-
|
|
848
|
-
this.
|
|
910
|
+
// A host's declaration names its parameters' types in its parameter list and its result's after
|
|
911
|
+
// it; text that is no plain function declaration declares every type it names with it.
|
|
912
|
+
const parts = signatureParts(entry.declaration, entry.name);
|
|
913
|
+
const input = this.scanTypes(parts?.params ?? entry.declaration);
|
|
914
|
+
const output = parts ? this.scanTypes(parts.result) : [];
|
|
915
|
+
rendered = { text: entry.declaration.trim(), types: [...new Set([...input, ...output])].sort(compareCodePoints) };
|
|
916
|
+
this.methodTypeSets.set(entry.path, { input, output });
|
|
849
917
|
} else {
|
|
850
918
|
const inputWriter = new SchemaDeclarationWriter(this.scopeContext(entry.inputScope));
|
|
851
919
|
const members = inputWriter.objectMembers(entry.input.json);
|
|
@@ -879,9 +947,10 @@ class FlexToolCatalogCore {
|
|
|
879
947
|
return rendered;
|
|
880
948
|
}
|
|
881
949
|
|
|
950
|
+
/** The catalog types a declaration's code names; its comments and string literals name none. */
|
|
882
951
|
private scanTypes(text: string, self?: string): string[] {
|
|
883
952
|
const found = new Set<string>();
|
|
884
|
-
for (const match of text.matchAll(identifierScanPattern)) {
|
|
953
|
+
for (const match of codeOf(text).matchAll(identifierScanPattern)) {
|
|
885
954
|
if (match[0] !== self && this.types.has(match[0])) found.add(match[0]);
|
|
886
955
|
}
|
|
887
956
|
return [...found].sort(compareCodePoints);
|
|
@@ -980,6 +1049,16 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
980
1049
|
private readonly describeTopMatch: boolean,
|
|
981
1050
|
) {}
|
|
982
1051
|
|
|
1052
|
+
/**
|
|
1053
|
+
* The identifier a name from elsewhere, such as a file or an API's method, is declared under as a
|
|
1054
|
+
* path segment or a type: every other character `_`, `_` before a leading digit and after a
|
|
1055
|
+
* reserved word (`export-jobs` becomes `export_jobs`, `delete` becomes `delete_`). The catalog
|
|
1056
|
+
* maps type names that are no identifiers the same way.
|
|
1057
|
+
*/
|
|
1058
|
+
public static identifierOf(name: string): string {
|
|
1059
|
+
return typeIdentifierOf(name);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
983
1062
|
/**
|
|
984
1063
|
* Prepares `catalog`. Preparing the same object again returns the cached preparation, so a host
|
|
985
1064
|
* that returns one catalog object for every run pays for it once.
|
|
@@ -987,6 +1066,8 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
987
1066
|
public static async create<TScope = unknown>(
|
|
988
1067
|
catalog: IFlexToolCatalog<TScope>,
|
|
989
1068
|
options: IFlexToolCatalogOptions = {},
|
|
1069
|
+
/** `results`: adds `results.get`, for a FlexHarness run that stores results. */
|
|
1070
|
+
builtIns: { results?: boolean } = {},
|
|
990
1071
|
): Promise<FlexToolCatalog<TScope>> {
|
|
991
1072
|
const normalized = normalizeToolCatalogOptions(options);
|
|
992
1073
|
const mode = normalized.declarations;
|
|
@@ -998,10 +1079,13 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
998
1079
|
byMode = new Map();
|
|
999
1080
|
coreCache.set(catalog, byMode);
|
|
1000
1081
|
}
|
|
1001
|
-
|
|
1082
|
+
const key = builtIns.results ? `${mode}+results` : mode;
|
|
1083
|
+
let core = byMode.get(key);
|
|
1002
1084
|
if (!core) {
|
|
1003
|
-
core =
|
|
1004
|
-
|
|
1085
|
+
core = builtIns.results
|
|
1086
|
+
? FlexToolCatalogCore.build(withResultsMethod(catalog), mode, new Set([FLEX_RESULT_METHOD]))
|
|
1087
|
+
: FlexToolCatalogCore.build(catalog, mode);
|
|
1088
|
+
byMode.set(key, core);
|
|
1005
1089
|
}
|
|
1006
1090
|
return new FlexToolCatalog<TScope>(
|
|
1007
1091
|
await core,
|
|
@@ -1038,6 +1122,16 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1038
1122
|
return this.core.methods.has(path) && this.isAdmitted(path);
|
|
1039
1123
|
}
|
|
1040
1124
|
|
|
1125
|
+
/** Whether the method at `path` declared that it only reads. */
|
|
1126
|
+
public isReadOnly(path: string): boolean {
|
|
1127
|
+
return this.has(path) && this.core.methods.get(path)!.readOnly;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/** Whether the method at `path` decides each call with `onCall`. */
|
|
1131
|
+
public decides(path: string): boolean {
|
|
1132
|
+
return this.has(path) && this.core.methods.get(path)!.onCall !== undefined;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1041
1135
|
/**
|
|
1042
1136
|
* The built-in tools as the model sees them: the three catalog tools, and `run_code` in code
|
|
1043
1137
|
* mode. Their text depends only on the catalog and its options.
|
|
@@ -1210,7 +1304,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1210
1304
|
const unknownTypes = requestedTypes.filter((name) => identifierOf(name) === undefined).sort(compareCodePoints);
|
|
1211
1305
|
const notes = [
|
|
1212
1306
|
...unknownMethods.map((path) => {
|
|
1213
|
-
const suggestions = closeMatches(path, this.
|
|
1307
|
+
const suggestions = closeMatches(path, this.listedMethods());
|
|
1214
1308
|
return `Unknown method ${path}.${suggestions.length ? ` Did you mean ${suggestions.join(', ')}?` : ' Find methods with search_tools.'}`;
|
|
1215
1309
|
}),
|
|
1216
1310
|
...unknownTypes.map((name) => {
|
|
@@ -1256,7 +1350,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1256
1350
|
error: {
|
|
1257
1351
|
error: 'unknown_method',
|
|
1258
1352
|
method: path,
|
|
1259
|
-
didYouMean: isNamespace ? [] : closeMatches(path, this.
|
|
1353
|
+
didYouMean: isNamespace ? [] : closeMatches(path, this.listedMethods()),
|
|
1260
1354
|
hint: isNamespace
|
|
1261
1355
|
? `${path} is a namespace; list it with search_tools({"namespace":"${path}"}).`
|
|
1262
1356
|
: 'Find methods with search_tools.',
|
|
@@ -1313,6 +1407,11 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1313
1407
|
return { method: input.method, params: input.params === undefined ? {} : input.params };
|
|
1314
1408
|
}
|
|
1315
1409
|
|
|
1410
|
+
/** The method paths search and suggestions may name: built-ins such as `results.get` stay out. */
|
|
1411
|
+
private listedMethods(): string[] {
|
|
1412
|
+
return this.methods.filter((path) => !this.core.unlisted.has(path));
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1316
1415
|
private isAdmitted(path: string): boolean {
|
|
1317
1416
|
return this.admitted === undefined || this.admitted.has(path);
|
|
1318
1417
|
}
|
|
@@ -1330,7 +1429,7 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1330
1429
|
};
|
|
1331
1430
|
node('');
|
|
1332
1431
|
for (const entry of this.core.methods.values()) {
|
|
1333
|
-
if (!this.isAdmitted(entry.path)) continue;
|
|
1432
|
+
if (!this.isAdmitted(entry.path) || this.core.unlisted.has(entry.path)) continue;
|
|
1334
1433
|
node(entry.namespace).methods.push(entry);
|
|
1335
1434
|
const segments = entry.path.split('.');
|
|
1336
1435
|
for (let depth = 1; depth < segments.length; depth++) {
|
|
@@ -1700,6 +1799,70 @@ export const catalogToolInputSchema = <TInput>(definition: IFlexCatalogToolDefin
|
|
|
1700
1799
|
});
|
|
1701
1800
|
};
|
|
1702
1801
|
|
|
1802
|
+
/** Declaration text with its comments and the contents of its string literals blanked. */
|
|
1803
|
+
const codeOf = (text: string): string => {
|
|
1804
|
+
let code = '';
|
|
1805
|
+
for (let index = 0; index < text.length; index++) {
|
|
1806
|
+
const char = text[index];
|
|
1807
|
+
if (char === '/' && text[index + 1] === '/') {
|
|
1808
|
+
const end = text.indexOf('\n', index);
|
|
1809
|
+
index = (end === -1 ? text.length : end) - 1;
|
|
1810
|
+
code += ' ';
|
|
1811
|
+
} else if (char === '/' && text[index + 1] === '*') {
|
|
1812
|
+
const end = text.indexOf('*/', index + 2);
|
|
1813
|
+
index = end === -1 ? text.length : end + 1;
|
|
1814
|
+
code += ' ';
|
|
1815
|
+
} else if (char === '"' || char === "'" || char === '`') {
|
|
1816
|
+
let end = index + 1;
|
|
1817
|
+
while (end < text.length && text[end] !== char) end += text[end] === '\\' ? 2 : 1;
|
|
1818
|
+
code += `${char}${char}`;
|
|
1819
|
+
index = end;
|
|
1820
|
+
} else {
|
|
1821
|
+
code += char;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return code;
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
/** The index of the bracket that closes the one at `open`, or -1. */
|
|
1828
|
+
const closingBracket = (code: string, open: number): number => {
|
|
1829
|
+
const pairs: Record<string, string> = { '(': ')', '<': '>', '{': '}', '[': ']' };
|
|
1830
|
+
const stack: string[] = [];
|
|
1831
|
+
for (let index = open; index < code.length; index++) {
|
|
1832
|
+
const char = code[index];
|
|
1833
|
+
if (char === '=' && code[index + 1] === '>') {
|
|
1834
|
+
index++;
|
|
1835
|
+
} else if (pairs[char] !== undefined && (char !== '<' || stack.length === 0 || stack[stack.length - 1] === '>')) {
|
|
1836
|
+
stack.push(pairs[char]);
|
|
1837
|
+
} else if (char === stack[stack.length - 1]) {
|
|
1838
|
+
stack.pop();
|
|
1839
|
+
if (stack.length === 0) return index;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return -1;
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* A host method declaration's parameter list and the result after it, as code without comments, or
|
|
1847
|
+
* undefined where the text is no plain `function name(…): …` declaration.
|
|
1848
|
+
*/
|
|
1849
|
+
const signatureParts = (declaration: string, name: string): { params: string; result: string } | undefined => {
|
|
1850
|
+
const code = codeOf(declaration);
|
|
1851
|
+
const start = new RegExp(`\\bfunction\\s+${name}\\s*`).exec(code);
|
|
1852
|
+
if (!start) return undefined;
|
|
1853
|
+
let open = start.index + start[0].length;
|
|
1854
|
+
if (code[open] === '<') {
|
|
1855
|
+
const typeParameters = closingBracket(code, open);
|
|
1856
|
+
if (typeParameters === -1) return undefined;
|
|
1857
|
+
open = typeParameters + 1;
|
|
1858
|
+
while (/\s/.test(code[open] ?? '')) open++;
|
|
1859
|
+
}
|
|
1860
|
+
if (code[open] !== '(') return undefined;
|
|
1861
|
+
const close = closingBracket(code, open);
|
|
1862
|
+
if (close === -1) return undefined;
|
|
1863
|
+
return { params: code.slice(open + 1, close), result: code.slice(close + 1) };
|
|
1864
|
+
};
|
|
1865
|
+
|
|
1703
1866
|
/** A root-level method declaration gets `declare`, as a namespace member does not need it. */
|
|
1704
1867
|
const withDeclare = (text: string): string => {
|
|
1705
1868
|
const lines = text.split('\n');
|
package/ts/interfaces.ts
CHANGED
|
@@ -139,6 +139,11 @@ export interface IFlexToolMessagePart {
|
|
|
139
139
|
childSessionId?: string;
|
|
140
140
|
/** For a catalog call a `run_code` program made: the `run_code` call's ID. */
|
|
141
141
|
parentToolCallId?: string;
|
|
142
|
+
/**
|
|
143
|
+
* For a read-only call an identical read earlier in the turn answered: that call's ID. The call
|
|
144
|
+
* did not run; its output is the earlier call's.
|
|
145
|
+
*/
|
|
146
|
+
deduplicatedFrom?: string;
|
|
142
147
|
model?: IFlexModelIdentity;
|
|
143
148
|
/** ISO time the tool call began. Absent on parts recorded before part times existed. */
|
|
144
149
|
startedAt?: string;
|
|
@@ -660,6 +665,14 @@ export interface IFlexToolHandle<TScope = unknown> {
|
|
|
660
665
|
* every run: FlexHarness prepares a catalog once per object and treats it as immutable.
|
|
661
666
|
*/
|
|
662
667
|
catalog?: IFlexToolCatalog<TScope>;
|
|
668
|
+
/**
|
|
669
|
+
* Tells read-only calls apart, by the tool's name or a catalog method's path and the call's
|
|
670
|
+
* input or params: they have no effect but their result. They run concurrently, may start while
|
|
671
|
+
* the model still streams its response, and a turn answers identical ones once. Every other call
|
|
672
|
+
* runs in the order the model made it, after the calls before it. A catalog method's `readOnly`
|
|
673
|
+
* declaration needs no classifier. It may be asked more than once about one call.
|
|
674
|
+
*/
|
|
675
|
+
isReadOnlyToolCall?(call: { toolName: string; input: unknown }): boolean;
|
|
663
676
|
close?(): Promise<void> | void;
|
|
664
677
|
}
|
|
665
678
|
|
|
@@ -669,6 +682,31 @@ export interface IFlexToolProvider<TScope> {
|
|
|
669
682
|
): Promise<IFlexToolHandle<TScope> | undefined> | IFlexToolHandle<TScope> | undefined;
|
|
670
683
|
}
|
|
671
684
|
|
|
685
|
+
/** The catalog method that reads stored results, reserved while a run stores results. */
|
|
686
|
+
export const FLEX_RESULT_METHOD = 'results.get';
|
|
687
|
+
|
|
688
|
+
export const FLEX_RESULT_LIMITS = Object.freeze({
|
|
689
|
+
/** The most of one output a result stores; beyond it the stored value is truncated. */
|
|
690
|
+
maxStoredBytes: 8 * 1024 * 1024,
|
|
691
|
+
/** The results a session keeps; a new one beyond them deletes the oldest. */
|
|
692
|
+
maxResultsPerSession: 32,
|
|
693
|
+
/** The size of the summary the model gets in place of the output. */
|
|
694
|
+
summaryBytes: 2 * 1024,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
/** What the model gets in place of an output over the budget. */
|
|
698
|
+
export interface IFlexResultHandle {
|
|
699
|
+
$flexType: 'result';
|
|
700
|
+
handle: string;
|
|
701
|
+
/** The stored output's size as JSON. */
|
|
702
|
+
bytes: number;
|
|
703
|
+
/** The pages `results.get` reads the whole output in. */
|
|
704
|
+
pages: number;
|
|
705
|
+
summary: TJsonValue;
|
|
706
|
+
/** How to read it. */
|
|
707
|
+
read: string;
|
|
708
|
+
}
|
|
709
|
+
|
|
672
710
|
/** The names of the built-in catalog tools, reserved while a run has a catalog. */
|
|
673
711
|
export const FLEX_TOOL_CATALOG_TOOL_NAMES = Object.freeze({
|
|
674
712
|
search: 'search_tools',
|
|
@@ -684,7 +722,7 @@ export const FLEX_TOOL_CATALOG_LIMITS = Object.freeze({
|
|
|
684
722
|
maxIdentifierCharacters: 128,
|
|
685
723
|
maxSummaryCharacters: 120,
|
|
686
724
|
maxDescriptionBytes: 16 * 1024,
|
|
687
|
-
maxDeclarationBytes:
|
|
725
|
+
maxDeclarationBytes: 64 * 1024,
|
|
688
726
|
maxTags: 16,
|
|
689
727
|
maxTagCharacters: 48,
|
|
690
728
|
maxAliases: 16,
|
|
@@ -826,6 +864,14 @@ export interface IFlexCatalogMethod<TScope = unknown, TParams = any, TResult = u
|
|
|
826
864
|
params: TParams,
|
|
827
865
|
context: IFlexCatalogRunContext<TScope>,
|
|
828
866
|
): TResult | PromiseLike<TResult> | AsyncIterable<TResult>;
|
|
867
|
+
/**
|
|
868
|
+
* The method only reads: it has no effect but its result. Its calls run concurrently with other
|
|
869
|
+
* read-only calls, `execute` may start one while the model still streams its response, and a
|
|
870
|
+
* turn answers identical calls once. Calls of other methods, and of a method with `onCall`, run
|
|
871
|
+
* in the order the model made them. A method that asks for permission is best left undeclared:
|
|
872
|
+
* the prompts of concurrent calls would overlap.
|
|
873
|
+
*/
|
|
874
|
+
readOnly?: boolean;
|
|
829
875
|
/**
|
|
830
876
|
* Decides each validated call before it runs: `run` (the default without this hook), `propose`
|
|
831
877
|
* to hand it to the catalog's `onProposals` instead of running it, or `{ deny }` to refuse it.
|
|
@@ -1386,6 +1432,22 @@ export interface IFlexToolJobStoreProvider {
|
|
|
1386
1432
|
deleteSession(storageKey: string, sessionId: string): Promise<void>;
|
|
1387
1433
|
}
|
|
1388
1434
|
|
|
1435
|
+
/**
|
|
1436
|
+
* Tool outputs over the output budget, stored whole for the session that produced them. A result
|
|
1437
|
+
* is written once under its handle and read by `results.get`; FlexHarness keeps a session's latest
|
|
1438
|
+
* `FLEX_RESULT_LIMITS.maxResultsPerSession` results and deletes older ones. A FlexHarness writes a
|
|
1439
|
+
* session's results one operation at a time, so a store need not order concurrent writes of one
|
|
1440
|
+
* session.
|
|
1441
|
+
*/
|
|
1442
|
+
export interface IFlexResultStore {
|
|
1443
|
+
save(storageKey: string, sessionId: string, handle: string, value: TJsonValue): Promise<void>;
|
|
1444
|
+
load(storageKey: string, sessionId: string, handle: string): Promise<TJsonValue | undefined>;
|
|
1445
|
+
/** The handles of the session's results, oldest first. */
|
|
1446
|
+
list(storageKey: string, sessionId: string): Promise<string[]>;
|
|
1447
|
+
delete(storageKey: string, sessionId: string, handle: string): Promise<void>;
|
|
1448
|
+
deleteSession(storageKey: string, sessionId: string): Promise<void>;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1389
1451
|
export interface IFlexHarnessStores {
|
|
1390
1452
|
scopes: IFlexScopeStore;
|
|
1391
1453
|
projections: IFlexProjectionStore;
|
|
@@ -1393,6 +1455,12 @@ export interface IFlexHarnessStores {
|
|
|
1393
1455
|
projectManagement: IFlexProjectManagementStore;
|
|
1394
1456
|
agentEvents: IFlexAgentEventStoreProvider;
|
|
1395
1457
|
jobs: IFlexToolJobStoreProvider;
|
|
1458
|
+
/**
|
|
1459
|
+
* Stores the outputs of a run with a tool catalog that exceed the output budget: the model gets a
|
|
1460
|
+
* summary and a handle it reads with `results.get`. Without it, such outputs are truncated to
|
|
1461
|
+
* the budget. The built-in stores provide it.
|
|
1462
|
+
*/
|
|
1463
|
+
results?: IFlexResultStore;
|
|
1396
1464
|
}
|
|
1397
1465
|
|
|
1398
1466
|
export interface IFlexJsonLimits {
|
package/ts/plugins.ts
CHANGED
|
@@ -97,6 +97,7 @@ import {
|
|
|
97
97
|
AgentPendingInputQueue,
|
|
98
98
|
AgentPendingInputRejectedError,
|
|
99
99
|
AgentSession,
|
|
100
|
+
AgentToolCallScheduler,
|
|
100
101
|
buildModelMessages,
|
|
101
102
|
ToolJobStoreConflictError,
|
|
102
103
|
createAgentEvent,
|
|
@@ -115,6 +116,7 @@ export {
|
|
|
115
116
|
AgentPendingInputQueue,
|
|
116
117
|
AgentPendingInputRejectedError,
|
|
117
118
|
AgentSession,
|
|
119
|
+
AgentToolCallScheduler,
|
|
118
120
|
buildModelMessages,
|
|
119
121
|
ToolJobStoreConflictError,
|
|
120
122
|
createAgentEvent,
|
|
@@ -142,6 +144,7 @@ export type {
|
|
|
142
144
|
IAgentSession,
|
|
143
145
|
IAgentSessionOptions,
|
|
144
146
|
IAgentToolCallUpdateEvent,
|
|
147
|
+
IAgentToolExecutionOptions,
|
|
145
148
|
IAgentRuntimeEventPayload,
|
|
146
149
|
IGenerationBegunEvent,
|
|
147
150
|
IGenerationExecutionCompletedEvent,
|