@stackline/tool-router 1.0.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/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +34 -0
- package/LICENSE +21 -0
- package/NOTICE +5 -0
- package/README.md +336 -0
- package/SECURITY.md +33 -0
- package/dist/index.cjs +1123 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.cts +247 -0
- package/dist/index.d.mts +247 -0
- package/dist/index.d.ts +247 -0
- package/dist/index.js +1102 -0
- package/dist/index.js.map +7 -0
- package/dist/index.min.js +3 -0
- package/dist/index.min.js.map +7 -0
- package/package.json +100 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
/*! @stackline/tool-router v1.0.0 | MIT */
|
|
2
|
+
|
|
3
|
+
// src/errors.js
|
|
4
|
+
var ToolRouterError = class extends Error {
|
|
5
|
+
constructor(code, message, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ToolRouterError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
if (details !== void 0) this.details = details;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function fail(code, message, details) {
|
|
13
|
+
throw new ToolRouterError(code, message, details);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/safe.js
|
|
17
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
18
|
+
function isUnsafeKey(key) {
|
|
19
|
+
return typeof key === "string" && UNSAFE_KEYS.has(key);
|
|
20
|
+
}
|
|
21
|
+
function isObject(value) {
|
|
22
|
+
return value !== null && typeof value === "object";
|
|
23
|
+
}
|
|
24
|
+
function ownValue(value, key) {
|
|
25
|
+
if (!isObject(value) && typeof value !== "function") return void 0;
|
|
26
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
27
|
+
return descriptor && Object.prototype.hasOwnProperty.call(descriptor, "value") ? descriptor.value : void 0;
|
|
28
|
+
}
|
|
29
|
+
function ownEnumerableEntries(value) {
|
|
30
|
+
if (!isObject(value)) return [];
|
|
31
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
32
|
+
const entries = [];
|
|
33
|
+
for (const key of Object.keys(descriptors)) {
|
|
34
|
+
const descriptor = descriptors[key];
|
|
35
|
+
if (!descriptor.enumerable || !Object.prototype.hasOwnProperty.call(descriptor, "value")) continue;
|
|
36
|
+
if (isUnsafeKey(key)) continue;
|
|
37
|
+
entries.push([key, descriptor.value]);
|
|
38
|
+
}
|
|
39
|
+
return entries;
|
|
40
|
+
}
|
|
41
|
+
function stringValue(value, fallback = "") {
|
|
42
|
+
return typeof value === "string" ? value : fallback;
|
|
43
|
+
}
|
|
44
|
+
function stringList(value, limit = 256) {
|
|
45
|
+
if (!Array.isArray(value)) return [];
|
|
46
|
+
const result = [];
|
|
47
|
+
for (let index = 0; index < value.length && result.length < limit; index++) {
|
|
48
|
+
const item = ownValue(value, String(index));
|
|
49
|
+
if (typeof item === "string" && item.length > 0) result.push(item);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
function positiveInteger(value, fallback, maximum, label) {
|
|
54
|
+
if (value === void 0) return fallback;
|
|
55
|
+
if (!Number.isInteger(value) || value < 1 || value > maximum) {
|
|
56
|
+
fail("ERR_TOOL_ROUTER_OPTION", `${label} must be an integer between 1 and ${maximum}`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function finiteNumber(value, fallback, minimum, maximum, label) {
|
|
61
|
+
if (value === void 0) return fallback;
|
|
62
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) {
|
|
63
|
+
fail("ERR_TOOL_ROUTER_OPTION", `${label} must be a finite number between ${minimum} and ${maximum}`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/adapters.js
|
|
69
|
+
var TOOL_FORMATS = Object.freeze([
|
|
70
|
+
"canonical",
|
|
71
|
+
"mcp",
|
|
72
|
+
"openai-chat",
|
|
73
|
+
"openai-responses",
|
|
74
|
+
"anthropic",
|
|
75
|
+
"gemini"
|
|
76
|
+
]);
|
|
77
|
+
function explicitFormat(format) {
|
|
78
|
+
if (format === void 0 || format === "auto") return void 0;
|
|
79
|
+
if (!TOOL_FORMATS.includes(format)) {
|
|
80
|
+
fail("ERR_TOOL_FORMAT", `Unsupported tool format: ${String(format)}`);
|
|
81
|
+
}
|
|
82
|
+
return format;
|
|
83
|
+
}
|
|
84
|
+
function detectToolFormat(tool) {
|
|
85
|
+
if (!isObject(tool)) fail("ERR_TOOL_DEFINITION", "Tool definition must be an object");
|
|
86
|
+
const type = ownValue(tool, "type");
|
|
87
|
+
const nestedFunction = ownValue(tool, "function");
|
|
88
|
+
if (type === "function" && isObject(nestedFunction)) return "openai-chat";
|
|
89
|
+
if (type === "function" && typeof ownValue(tool, "name") === "string") return "openai-responses";
|
|
90
|
+
if (ownValue(tool, "input_schema") !== void 0) return "anthropic";
|
|
91
|
+
if (ownValue(tool, "inputSchema") !== void 0) return "mcp";
|
|
92
|
+
if (ownValue(tool, "parameters") !== void 0) return "gemini";
|
|
93
|
+
if (typeof ownValue(tool, "name") === "string") return "canonical";
|
|
94
|
+
fail("ERR_TOOL_FORMAT", "Unable to detect the tool definition format");
|
|
95
|
+
}
|
|
96
|
+
function sourceForFormat(tool, format) {
|
|
97
|
+
if (format === "openai-chat") {
|
|
98
|
+
const nested = ownValue(tool, "function");
|
|
99
|
+
if (!isObject(nested)) fail("ERR_TOOL_DEFINITION", "OpenAI Chat tool.function must be an object");
|
|
100
|
+
return nested;
|
|
101
|
+
}
|
|
102
|
+
return tool;
|
|
103
|
+
}
|
|
104
|
+
function schemaForFormat(source, format) {
|
|
105
|
+
if (format === "mcp" || format === "canonical") {
|
|
106
|
+
return ownValue(source, "inputSchema") === void 0 ? ownValue(source, "schema") : ownValue(source, "inputSchema");
|
|
107
|
+
}
|
|
108
|
+
if (format === "anthropic") return ownValue(source, "input_schema");
|
|
109
|
+
return ownValue(source, "parameters");
|
|
110
|
+
}
|
|
111
|
+
function outputSchemaForFormat(source, format) {
|
|
112
|
+
if (format === "mcp" || format === "canonical") return ownValue(source, "outputSchema");
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
function inferNamespace(name) {
|
|
116
|
+
for (const separator of ["__", ".", "/"]) {
|
|
117
|
+
const index = name.indexOf(separator);
|
|
118
|
+
if (index > 0) return name.slice(0, index);
|
|
119
|
+
}
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
function normalizeTool(tool, options = {}) {
|
|
123
|
+
if (!isObject(tool)) fail("ERR_TOOL_DEFINITION", "Tool definition must be an object");
|
|
124
|
+
if (!isObject(options)) fail("ERR_TOOL_ROUTER_OPTION", "Normalization options must be an object");
|
|
125
|
+
const format = explicitFormat(options.format) || detectToolFormat(tool);
|
|
126
|
+
const source = sourceForFormat(tool, format);
|
|
127
|
+
const name = stringValue(ownValue(source, "name"));
|
|
128
|
+
if (name.length === 0) fail("ERR_TOOL_NAME", "Tool name must be a non-empty string");
|
|
129
|
+
if (name.length > 512) fail("ERR_TOOL_NAME", "Tool name must not exceed 512 characters");
|
|
130
|
+
const description = stringValue(ownValue(source, "description"));
|
|
131
|
+
const explicitNamespace = stringValue(ownValue(tool, "namespace")) || stringValue(ownValue(source, "namespace"));
|
|
132
|
+
const namespace = explicitNamespace || inferNamespace(name);
|
|
133
|
+
const explicitId = stringValue(ownValue(tool, "id")) || stringValue(ownValue(source, "id"));
|
|
134
|
+
const id = explicitId || (namespace ? `${namespace}:${name}` : name);
|
|
135
|
+
const tags = stringList(ownValue(tool, "tags")).concat(stringList(ownValue(source, "tags")));
|
|
136
|
+
const aliases = stringList(ownValue(tool, "aliases")).concat(stringList(ownValue(source, "aliases")));
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
aliases: Object.freeze(Array.from(new Set(aliases))),
|
|
139
|
+
description,
|
|
140
|
+
format,
|
|
141
|
+
id,
|
|
142
|
+
inputSchema: schemaForFormat(source, format),
|
|
143
|
+
name,
|
|
144
|
+
namespace,
|
|
145
|
+
original: tool,
|
|
146
|
+
outputSchema: outputSchemaForFormat(source, format),
|
|
147
|
+
tags: Object.freeze(Array.from(new Set(tags)))
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function unwrapTools(input) {
|
|
151
|
+
if (Array.isArray(input)) return input;
|
|
152
|
+
if (!isObject(input)) fail("ERR_TOOL_CATALOG", "Tool catalog must be an array or provider envelope");
|
|
153
|
+
const tools = ownValue(input, "tools");
|
|
154
|
+
if (Array.isArray(tools)) return tools;
|
|
155
|
+
const declarations = ownValue(input, "functionDeclarations");
|
|
156
|
+
if (Array.isArray(declarations)) return declarations;
|
|
157
|
+
fail("ERR_TOOL_CATALOG", "Tool catalog envelope must contain tools or functionDeclarations");
|
|
158
|
+
}
|
|
159
|
+
function normalizeTools(input, options = {}) {
|
|
160
|
+
if (!isObject(options)) fail("ERR_TOOL_ROUTER_OPTION", "Normalization options must be an object");
|
|
161
|
+
const outer = unwrapTools(input);
|
|
162
|
+
const maxTools = positiveInteger(options.maxTools, 1e5, 1e6, "maxTools");
|
|
163
|
+
if (outer.length > maxTools) {
|
|
164
|
+
fail("ERR_TOOL_CATALOG_SIZE", `Tool catalog exceeds maxTools (${maxTools})`);
|
|
165
|
+
}
|
|
166
|
+
const flattened = [];
|
|
167
|
+
for (let index = 0; index < outer.length; index++) {
|
|
168
|
+
const descriptor = Object.getOwnPropertyDescriptor(outer, String(index));
|
|
169
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, "value")) continue;
|
|
170
|
+
const value = descriptor.value;
|
|
171
|
+
if (isObject(value) && Array.isArray(ownValue(value, "functionDeclarations"))) {
|
|
172
|
+
const declarations = ownValue(value, "functionDeclarations");
|
|
173
|
+
if (declarations.length > maxTools - flattened.length) {
|
|
174
|
+
fail("ERR_TOOL_CATALOG_SIZE", `Tool catalog exceeds maxTools (${maxTools})`);
|
|
175
|
+
}
|
|
176
|
+
for (let inner = 0; inner < declarations.length; inner++) {
|
|
177
|
+
const innerDescriptor = Object.getOwnPropertyDescriptor(declarations, String(inner));
|
|
178
|
+
if (innerDescriptor && Object.prototype.hasOwnProperty.call(innerDescriptor, "value")) {
|
|
179
|
+
flattened.push(normalizeTool(innerDescriptor.value, { format: "gemini" }));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
flattened.push(normalizeTool(value, options));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return flattened;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/schema.js
|
|
190
|
+
var TEXT_KEYS = /* @__PURE__ */ new Set(["title", "description", "$comment", "format", "pattern", "const"]);
|
|
191
|
+
var COLLECTION_KEYS = /* @__PURE__ */ new Set(["enum", "examples", "required"]);
|
|
192
|
+
function collectSchemaText(schema, options = {}) {
|
|
193
|
+
if (!isObject(schema) && typeof schema !== "boolean") return "";
|
|
194
|
+
const maxDepth = options.maxSchemaDepth === void 0 ? 24 : options.maxSchemaDepth;
|
|
195
|
+
const maxNodes = options.maxSchemaNodes === void 0 ? 1e4 : options.maxSchemaNodes;
|
|
196
|
+
const maxTextLength = options.maxTextLength === void 0 ? 65536 : options.maxTextLength;
|
|
197
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
198
|
+
const parts = [];
|
|
199
|
+
let nodes = 0;
|
|
200
|
+
let length = 0;
|
|
201
|
+
function consumeNode() {
|
|
202
|
+
nodes += 1;
|
|
203
|
+
if (nodes > maxNodes) fail("ERR_TOOL_SCHEMA_SIZE", `Tool schema exceeds maxSchemaNodes (${maxNodes})`);
|
|
204
|
+
}
|
|
205
|
+
function append(value) {
|
|
206
|
+
if (typeof value !== "string" && typeof value !== "number") return;
|
|
207
|
+
const text = String(value);
|
|
208
|
+
if (text.length === 0 || length >= maxTextLength) return;
|
|
209
|
+
const remaining = maxTextLength - length;
|
|
210
|
+
const piece = text.slice(0, remaining);
|
|
211
|
+
parts.push(piece);
|
|
212
|
+
length += piece.length + 1;
|
|
213
|
+
}
|
|
214
|
+
function visit(value, depth, parentKey) {
|
|
215
|
+
if (depth > maxDepth) fail("ERR_TOOL_SCHEMA_DEPTH", `Tool schema exceeds maxSchemaDepth (${maxDepth})`);
|
|
216
|
+
consumeNode();
|
|
217
|
+
if (!isObject(value)) return;
|
|
218
|
+
if (seen.has(value)) return;
|
|
219
|
+
seen.add(value);
|
|
220
|
+
if (Array.isArray(value)) {
|
|
221
|
+
if (value.length > maxNodes - nodes) {
|
|
222
|
+
fail("ERR_TOOL_SCHEMA_SIZE", `Tool schema exceeds maxSchemaNodes (${maxNodes})`);
|
|
223
|
+
}
|
|
224
|
+
for (let index = 0; index < value.length; index++) {
|
|
225
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
226
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, "value")) continue;
|
|
227
|
+
if (COLLECTION_KEYS.has(parentKey)) {
|
|
228
|
+
consumeNode();
|
|
229
|
+
append(descriptor.value);
|
|
230
|
+
} else visit(descriptor.value, depth + 1, parentKey);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const entries = ownEnumerableEntries(value);
|
|
235
|
+
if (entries.length > maxNodes - nodes) {
|
|
236
|
+
fail("ERR_TOOL_SCHEMA_SIZE", `Tool schema exceeds maxSchemaNodes (${maxNodes})`);
|
|
237
|
+
}
|
|
238
|
+
for (const [key, child] of entries) {
|
|
239
|
+
if (parentKey === "properties" || parentKey === "$defs" || parentKey === "definitions") append(key);
|
|
240
|
+
if (TEXT_KEYS.has(key)) {
|
|
241
|
+
consumeNode();
|
|
242
|
+
append(child);
|
|
243
|
+
} else if (COLLECTION_KEYS.has(key) && Array.isArray(child)) visit(child, depth + 1, key);
|
|
244
|
+
else visit(child, depth + 1, key);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
visit(schema, 0, "");
|
|
248
|
+
return parts.join(" ");
|
|
249
|
+
}
|
|
250
|
+
function estimateJsonTokens(value, options = {}) {
|
|
251
|
+
const maxDepth = options.maxDepth === void 0 ? 64 : options.maxDepth;
|
|
252
|
+
const maxNodes = options.maxNodes === void 0 ? 1e5 : options.maxNodes;
|
|
253
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
254
|
+
let nodes = 0;
|
|
255
|
+
function stringLength(text) {
|
|
256
|
+
return JSON.stringify(text).length;
|
|
257
|
+
}
|
|
258
|
+
function measure(current, depth) {
|
|
259
|
+
if (depth > maxDepth) fail("ERR_TOOL_VALUE_DEPTH", `Tool definition exceeds maxDepth (${maxDepth})`);
|
|
260
|
+
nodes += 1;
|
|
261
|
+
if (nodes > maxNodes) fail("ERR_TOOL_VALUE_SIZE", `Tool definition exceeds maxNodes (${maxNodes})`);
|
|
262
|
+
if (current === null) return 4;
|
|
263
|
+
if (typeof current === "string") return stringLength(current);
|
|
264
|
+
if (typeof current === "number") return Number.isFinite(current) ? String(current).length : 4;
|
|
265
|
+
if (typeof current === "boolean") return current ? 4 : 5;
|
|
266
|
+
if (typeof current === "bigint") fail("ERR_TOOL_VALUE_TYPE", "Tool definition cannot contain bigint values");
|
|
267
|
+
if (typeof current === "undefined" || typeof current === "function" || typeof current === "symbol") return 0;
|
|
268
|
+
if (seen.has(current)) fail("ERR_TOOL_CYCLIC", "Tool definition must be JSON-serializable");
|
|
269
|
+
seen.add(current);
|
|
270
|
+
let length = 2;
|
|
271
|
+
let count = 0;
|
|
272
|
+
if (Array.isArray(current)) {
|
|
273
|
+
if (current.length > maxNodes - nodes) {
|
|
274
|
+
fail("ERR_TOOL_VALUE_SIZE", `Tool definition exceeds maxNodes (${maxNodes})`);
|
|
275
|
+
}
|
|
276
|
+
for (let index = 0; index < current.length; index++) {
|
|
277
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, String(index));
|
|
278
|
+
const child = descriptor && Object.prototype.hasOwnProperty.call(descriptor, "value") ? descriptor.value : null;
|
|
279
|
+
length += measure(child, depth + 1) + (count > 0 ? 1 : 0);
|
|
280
|
+
count += 1;
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
const entries = ownEnumerableEntries(current);
|
|
284
|
+
if (entries.length > maxNodes - nodes) {
|
|
285
|
+
fail("ERR_TOOL_VALUE_SIZE", `Tool definition exceeds maxNodes (${maxNodes})`);
|
|
286
|
+
}
|
|
287
|
+
for (const [key, child] of entries) {
|
|
288
|
+
const measured = measure(child, depth + 1);
|
|
289
|
+
if (measured === 0 && (child === void 0 || typeof child === "function" || typeof child === "symbol")) continue;
|
|
290
|
+
length += stringLength(key) + 1 + measured + (count > 0 ? 1 : 0);
|
|
291
|
+
count += 1;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
seen.delete(current);
|
|
295
|
+
return length;
|
|
296
|
+
}
|
|
297
|
+
return Math.max(1, Math.ceil(measure(value, 0) / 4));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/tokenize.js
|
|
301
|
+
var COMBINING_MARKS = /[\u0300-\u036f]/g;
|
|
302
|
+
var CAMEL_BOUNDARY = /([\p{Ll}\p{N}])([\p{Lu}])/gu;
|
|
303
|
+
var ACRONYM_BOUNDARY = /([\p{Lu}]+)([\p{Lu}][\p{Ll}])/gu;
|
|
304
|
+
var WORDS = /[\p{L}\p{N}]+/gu;
|
|
305
|
+
var CJK = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uac00-\ud7af]/u;
|
|
306
|
+
function normalizeText(input) {
|
|
307
|
+
if (typeof input !== "string") return "";
|
|
308
|
+
let value = input;
|
|
309
|
+
if (typeof value.normalize === "function") value = value.normalize("NFKD");
|
|
310
|
+
return value.replace(COMBINING_MARKS, "").replace(ACRONYM_BOUNDARY, "$1 $2").replace(CAMEL_BOUNDARY, "$1 $2").toLocaleLowerCase("en-US");
|
|
311
|
+
}
|
|
312
|
+
function normalizeIdentifier(input) {
|
|
313
|
+
return normalizeText(input).replace(/[^\p{L}\p{N}]+/gu, "");
|
|
314
|
+
}
|
|
315
|
+
function tokenize(input, options = {}) {
|
|
316
|
+
const maxTokenLength = options.maxTokenLength === void 0 ? 64 : options.maxTokenLength;
|
|
317
|
+
if (!Number.isInteger(maxTokenLength) || maxTokenLength < 2 || maxTokenLength > 256) {
|
|
318
|
+
fail("ERR_TOOL_ROUTER_OPTION", "maxTokenLength must be an integer between 2 and 256");
|
|
319
|
+
}
|
|
320
|
+
const normalized = normalizeText(input);
|
|
321
|
+
const words = normalized.match(WORDS) || [];
|
|
322
|
+
const tokens = [];
|
|
323
|
+
for (const word of words) {
|
|
324
|
+
const token = word.slice(0, maxTokenLength);
|
|
325
|
+
if (token.length > 1 || CJK.test(token)) tokens.push(token);
|
|
326
|
+
if (token.length > 2 && CJK.test(token)) {
|
|
327
|
+
const characters = Array.from(token);
|
|
328
|
+
for (let index = 0; index < characters.length - 1; index++) {
|
|
329
|
+
tokens.push(`${characters[index]}${characters[index + 1]}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return tokens;
|
|
334
|
+
}
|
|
335
|
+
function tokenTrigrams(token) {
|
|
336
|
+
const characters = Array.from(` ${token} `);
|
|
337
|
+
const result = [];
|
|
338
|
+
for (let index = 0; index <= characters.length - 3; index++) {
|
|
339
|
+
result.push(characters.slice(index, index + 3).join(""));
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
function boundedEditDistance(left, right, maximum) {
|
|
344
|
+
const a = Array.from(left);
|
|
345
|
+
const b = Array.from(right);
|
|
346
|
+
if (Math.abs(a.length - b.length) > maximum) return maximum + 1;
|
|
347
|
+
if (a.length === 0) return Math.min(b.length, maximum + 1);
|
|
348
|
+
if (b.length === 0) return Math.min(a.length, maximum + 1);
|
|
349
|
+
let previous = new Array(b.length + 1);
|
|
350
|
+
for (let index = 0; index <= b.length; index++) previous[index] = index;
|
|
351
|
+
for (let row = 1; row <= a.length; row++) {
|
|
352
|
+
const current = new Array(b.length + 1);
|
|
353
|
+
current[0] = row;
|
|
354
|
+
let rowMinimum = current[0];
|
|
355
|
+
for (let column = 1; column <= b.length; column++) {
|
|
356
|
+
const substitution = previous[column - 1] + (a[row - 1] === b[column - 1] ? 0 : 1);
|
|
357
|
+
const insertion = current[column - 1] + 1;
|
|
358
|
+
const deletion = previous[column] + 1;
|
|
359
|
+
current[column] = Math.min(substitution, insertion, deletion);
|
|
360
|
+
rowMinimum = Math.min(rowMinimum, current[column]);
|
|
361
|
+
}
|
|
362
|
+
if (rowMinimum > maximum) return maximum + 1;
|
|
363
|
+
previous = current;
|
|
364
|
+
}
|
|
365
|
+
return previous[b.length];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/router.js
|
|
369
|
+
var FIELD_NAMES = Object.freeze(["name", "namespace", "aliases", "tags", "description", "schema"]);
|
|
370
|
+
var DEFAULT_WEIGHTS = Object.freeze({
|
|
371
|
+
aliases: 7,
|
|
372
|
+
description: 2,
|
|
373
|
+
name: 10,
|
|
374
|
+
namespace: 8,
|
|
375
|
+
schema: 1,
|
|
376
|
+
tags: 5
|
|
377
|
+
});
|
|
378
|
+
var DEFAULT_SYNONYM_GROUPS = Object.freeze([
|
|
379
|
+
["cancel", "close", "delete", "remove"],
|
|
380
|
+
["create", "add", "make", "new", "open", "schedule"],
|
|
381
|
+
["download", "fetch", "get", "read", "retrieve"],
|
|
382
|
+
["find", "discover", "look", "lookup", "query", "search"],
|
|
383
|
+
["list", "browse", "show"],
|
|
384
|
+
["send", "deliver", "post", "publish"],
|
|
385
|
+
["update", "change", "edit", "modify", "set"],
|
|
386
|
+
["available", "availability", "free"],
|
|
387
|
+
["conversation", "conversations", "message", "messages"],
|
|
388
|
+
["document", "documents", "file", "files", "page", "pages"]
|
|
389
|
+
]);
|
|
390
|
+
function resolveWeights(input) {
|
|
391
|
+
const weights = /* @__PURE__ */ Object.create(null);
|
|
392
|
+
for (const field of FIELD_NAMES) {
|
|
393
|
+
const value = input && Object.prototype.hasOwnProperty.call(input, field) ? input[field] : DEFAULT_WEIGHTS[field];
|
|
394
|
+
weights[field] = finiteNumber(value, DEFAULT_WEIGHTS[field], 0, 100, `fieldWeights.${field}`);
|
|
395
|
+
}
|
|
396
|
+
return Object.freeze(weights);
|
|
397
|
+
}
|
|
398
|
+
function resolveSynonyms(input) {
|
|
399
|
+
if (input !== void 0 && input !== false && !isObject(input)) {
|
|
400
|
+
fail("ERR_TOOL_ROUTER_OPTION", "synonyms must be false or an object of string arrays");
|
|
401
|
+
}
|
|
402
|
+
const synonyms = /* @__PURE__ */ new Map();
|
|
403
|
+
function connect(words) {
|
|
404
|
+
const unique = Array.from(new Set(words.filter((word) => typeof word === "string" && word.length > 1)));
|
|
405
|
+
for (const word of unique) {
|
|
406
|
+
let related = synonyms.get(word);
|
|
407
|
+
if (!related) {
|
|
408
|
+
related = /* @__PURE__ */ new Set();
|
|
409
|
+
synonyms.set(word, related);
|
|
410
|
+
}
|
|
411
|
+
for (const candidate of unique) if (candidate !== word) related.add(candidate);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (input !== false) for (const group of DEFAULT_SYNONYM_GROUPS) connect(group);
|
|
415
|
+
if (isObject(input)) {
|
|
416
|
+
for (const [word, values] of ownEnumerableEntries(input)) {
|
|
417
|
+
const normalizedWord = tokenize(word)[0];
|
|
418
|
+
if (!normalizedWord) continue;
|
|
419
|
+
if (typeof values !== "string" && !Array.isArray(values)) {
|
|
420
|
+
fail("ERR_TOOL_ROUTER_OPTION", `synonyms.${word} must be a string or array of strings`);
|
|
421
|
+
}
|
|
422
|
+
const normalizedValues = [];
|
|
423
|
+
for (const value of stringList(typeof values === "string" ? [values] : values)) {
|
|
424
|
+
normalizedValues.push(...tokenize(value));
|
|
425
|
+
}
|
|
426
|
+
connect([normalizedWord, ...normalizedValues]);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
430
|
+
for (const [word, related] of synonyms) result[word] = Object.freeze(Array.from(related));
|
|
431
|
+
return Object.freeze(result);
|
|
432
|
+
}
|
|
433
|
+
function resolveOptions(options = {}) {
|
|
434
|
+
if (options === null || typeof options !== "object") {
|
|
435
|
+
fail("ERR_TOOL_ROUTER_OPTION", "Router options must be an object");
|
|
436
|
+
}
|
|
437
|
+
const tokenizer = options.tokenizer === void 0 ? tokenize : options.tokenizer;
|
|
438
|
+
if (typeof tokenizer !== "function") fail("ERR_TOOL_ROUTER_OPTION", "tokenizer must be a function");
|
|
439
|
+
const onDuplicate = options.onDuplicate === void 0 ? "error" : options.onDuplicate;
|
|
440
|
+
if (onDuplicate !== "error" && onDuplicate !== "replace") {
|
|
441
|
+
fail("ERR_TOOL_ROUTER_OPTION", "onDuplicate must be error or replace");
|
|
442
|
+
}
|
|
443
|
+
return Object.freeze({
|
|
444
|
+
b: finiteNumber(options.b, 0.75, 0, 1, "b"),
|
|
445
|
+
fieldWeights: resolveWeights(options.fieldWeights),
|
|
446
|
+
format: options.format,
|
|
447
|
+
fuzzy: options.fuzzy === void 0 ? true : Boolean(options.fuzzy),
|
|
448
|
+
k1: finiteNumber(options.k1, 1.2, 0.1, 5, "k1"),
|
|
449
|
+
maxExpansions: positiveInteger(options.maxExpansions, 12, 100, "maxExpansions"),
|
|
450
|
+
maxQueryLength: positiveInteger(options.maxQueryLength, 4096, 65536, "maxQueryLength"),
|
|
451
|
+
maxSchemaDepth: positiveInteger(options.maxSchemaDepth, 24, 256, "maxSchemaDepth"),
|
|
452
|
+
maxSchemaNodes: positiveInteger(options.maxSchemaNodes, 1e4, 1e6, "maxSchemaNodes"),
|
|
453
|
+
maxTextLength: positiveInteger(options.maxTextLength, 65536, 1e6, "maxTextLength"),
|
|
454
|
+
maxTools: positiveInteger(options.maxTools, 1e5, 1e6, "maxTools"),
|
|
455
|
+
minFuzzyLength: positiveInteger(options.minFuzzyLength, 4, 32, "minFuzzyLength"),
|
|
456
|
+
onDuplicate,
|
|
457
|
+
synonyms: resolveSynonyms(options.synonyms),
|
|
458
|
+
tokenizer
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function countTokens(tokens) {
|
|
462
|
+
const counts = /* @__PURE__ */ new Map();
|
|
463
|
+
for (const token of tokens) counts.set(token, (counts.get(token) || 0) + 1);
|
|
464
|
+
return counts;
|
|
465
|
+
}
|
|
466
|
+
function nameSearchText(name) {
|
|
467
|
+
return `${name} ${normalizeIdentifier(name)}`;
|
|
468
|
+
}
|
|
469
|
+
function nameAcronyms(name) {
|
|
470
|
+
const words = tokenize(name).filter((word) => word.length > 0);
|
|
471
|
+
const acronyms = /* @__PURE__ */ new Set();
|
|
472
|
+
if (words.length > 1) {
|
|
473
|
+
acronyms.add(words.map((word) => Array.from(word)[0]).join(""));
|
|
474
|
+
for (let start = 0; start < words.length - 1; start++) {
|
|
475
|
+
for (let end = start + 2; end <= Math.min(words.length, start + 6); end++) {
|
|
476
|
+
acronyms.add(words.slice(start, end).map((word) => Array.from(word)[0]).join(""));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return acronyms;
|
|
481
|
+
}
|
|
482
|
+
function queryAcronyms(query) {
|
|
483
|
+
const acronyms = [];
|
|
484
|
+
const matches = query.match(/\b[A-Z][A-Z0-9]{1,7}\b/g) || [];
|
|
485
|
+
for (const match of matches) acronyms.push(`acr:${match.toLocaleLowerCase("en-US")}`);
|
|
486
|
+
return acronyms;
|
|
487
|
+
}
|
|
488
|
+
function toSet(value) {
|
|
489
|
+
if (value === void 0) return void 0;
|
|
490
|
+
const list = typeof value === "string" ? [value] : value;
|
|
491
|
+
if (!Array.isArray(list)) fail("ERR_TOOL_ROUTER_OPTION", "Search filters must be strings or arrays of strings");
|
|
492
|
+
return new Set(stringList(list, 1e4));
|
|
493
|
+
}
|
|
494
|
+
function compileFilters(options) {
|
|
495
|
+
const ids = toSet(options.ids);
|
|
496
|
+
const namespaces = toSet(options.namespaces);
|
|
497
|
+
const formats = toSet(options.formats);
|
|
498
|
+
const tags = toSet(options.tags);
|
|
499
|
+
const filter = options.filter;
|
|
500
|
+
if (filter !== void 0 && typeof filter !== "function") {
|
|
501
|
+
fail("ERR_TOOL_ROUTER_OPTION", "filter must be a function");
|
|
502
|
+
}
|
|
503
|
+
return (record) => {
|
|
504
|
+
if (ids && !ids.has(record.id)) return false;
|
|
505
|
+
if (namespaces && !namespaces.has(record.namespace)) return false;
|
|
506
|
+
if (formats && !formats.has(record.format)) return false;
|
|
507
|
+
if (tags) {
|
|
508
|
+
for (const tag of tags) if (!record.tags.includes(tag)) return false;
|
|
509
|
+
}
|
|
510
|
+
return filter === void 0 || Boolean(filter(record));
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function publicMatch(document, score, matchedTerms, matchedFields, queryCoverage, pinned = false) {
|
|
514
|
+
return Object.freeze({
|
|
515
|
+
estimatedTokens: document.estimatedTokens,
|
|
516
|
+
id: document.record.id,
|
|
517
|
+
matchedFields: Object.freeze(Array.from(matchedFields).sort()),
|
|
518
|
+
matchedTerms: Object.freeze(Array.from(matchedTerms).sort()),
|
|
519
|
+
name: document.record.name,
|
|
520
|
+
pinned,
|
|
521
|
+
queryCoverage,
|
|
522
|
+
record: document.record,
|
|
523
|
+
score: score === null ? null : Number(score.toFixed(6)),
|
|
524
|
+
tool: document.record.original
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
var ToolRouter = class _ToolRouter {
|
|
528
|
+
constructor(tools = [], options = {}) {
|
|
529
|
+
this._options = resolveOptions(options);
|
|
530
|
+
this._documents = /* @__PURE__ */ new Map();
|
|
531
|
+
this._postings = /* @__PURE__ */ new Map();
|
|
532
|
+
this._prefixes = /* @__PURE__ */ new Map();
|
|
533
|
+
this._trigrams = /* @__PURE__ */ new Map();
|
|
534
|
+
this._fieldTotals = /* @__PURE__ */ Object.create(null);
|
|
535
|
+
for (const field of FIELD_NAMES) this._fieldTotals[field] = 0;
|
|
536
|
+
this._estimatedTokens = 0;
|
|
537
|
+
if (Array.isArray(tools) && tools.length === 0) return;
|
|
538
|
+
this.add(tools);
|
|
539
|
+
}
|
|
540
|
+
get size() {
|
|
541
|
+
return this._documents.size;
|
|
542
|
+
}
|
|
543
|
+
get options() {
|
|
544
|
+
return this._options;
|
|
545
|
+
}
|
|
546
|
+
add(tools, options = {}) {
|
|
547
|
+
const catalog = isObject(tools) && (Array.isArray(ownValue(tools, "tools")) || Array.isArray(ownValue(tools, "functionDeclarations"))) ? tools : Array.isArray(tools) ? tools : [tools];
|
|
548
|
+
const records = normalizeTools(catalog, {
|
|
549
|
+
format: options.format === void 0 ? this._options.format : options.format,
|
|
550
|
+
maxTools: this._options.maxTools
|
|
551
|
+
});
|
|
552
|
+
const prepared = records.map((record) => this._prepare(record));
|
|
553
|
+
const incoming = /* @__PURE__ */ new Set();
|
|
554
|
+
for (const document of prepared) {
|
|
555
|
+
if (incoming.has(document.record.id)) {
|
|
556
|
+
fail("ERR_TOOL_DUPLICATE", `Duplicate tool id in batch: ${document.record.id}`);
|
|
557
|
+
}
|
|
558
|
+
incoming.add(document.record.id);
|
|
559
|
+
if (this._documents.has(document.record.id) && this._options.onDuplicate === "error") {
|
|
560
|
+
fail("ERR_TOOL_DUPLICATE", `Tool id already exists: ${document.record.id}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
let resultingSize = this._documents.size;
|
|
564
|
+
for (const id of incoming) if (!this._documents.has(id)) resultingSize += 1;
|
|
565
|
+
if (resultingSize > this._options.maxTools) {
|
|
566
|
+
fail("ERR_TOOL_CATALOG_SIZE", `Tool catalog exceeds maxTools (${this._options.maxTools})`);
|
|
567
|
+
}
|
|
568
|
+
for (const document of prepared) {
|
|
569
|
+
if (this._documents.has(document.record.id)) this._removeDocument(document.record.id);
|
|
570
|
+
this._addDocument(document);
|
|
571
|
+
}
|
|
572
|
+
return this;
|
|
573
|
+
}
|
|
574
|
+
remove(ids) {
|
|
575
|
+
const list = Array.isArray(ids) ? ids : [ids];
|
|
576
|
+
let removed = 0;
|
|
577
|
+
for (const id of list) {
|
|
578
|
+
if (typeof id !== "string") fail("ERR_TOOL_NAME", "Tool id must be a string");
|
|
579
|
+
const resolved = this._resolveDocument(id);
|
|
580
|
+
if (resolved && this._removeDocument(resolved.record.id)) removed += 1;
|
|
581
|
+
}
|
|
582
|
+
return removed;
|
|
583
|
+
}
|
|
584
|
+
replace(tools, options = {}) {
|
|
585
|
+
const fresh = new _ToolRouter([], this._options);
|
|
586
|
+
fresh.add(tools, options);
|
|
587
|
+
this._documents = fresh._documents;
|
|
588
|
+
this._postings = fresh._postings;
|
|
589
|
+
this._prefixes = fresh._prefixes;
|
|
590
|
+
this._trigrams = fresh._trigrams;
|
|
591
|
+
this._fieldTotals = fresh._fieldTotals;
|
|
592
|
+
this._estimatedTokens = fresh._estimatedTokens;
|
|
593
|
+
return this;
|
|
594
|
+
}
|
|
595
|
+
clear() {
|
|
596
|
+
this._documents.clear();
|
|
597
|
+
this._postings.clear();
|
|
598
|
+
this._prefixes.clear();
|
|
599
|
+
this._trigrams.clear();
|
|
600
|
+
for (const field of FIELD_NAMES) this._fieldTotals[field] = 0;
|
|
601
|
+
this._estimatedTokens = 0;
|
|
602
|
+
return this;
|
|
603
|
+
}
|
|
604
|
+
has(idOrName) {
|
|
605
|
+
return Boolean(this._resolveDocument(idOrName));
|
|
606
|
+
}
|
|
607
|
+
get(idOrName) {
|
|
608
|
+
const document = this._resolveDocument(idOrName);
|
|
609
|
+
return document ? document.record : void 0;
|
|
610
|
+
}
|
|
611
|
+
list() {
|
|
612
|
+
return Array.from(this._documents.values(), (document) => document.record);
|
|
613
|
+
}
|
|
614
|
+
stats() {
|
|
615
|
+
return Object.freeze({
|
|
616
|
+
estimatedTokens: this._estimatedTokens,
|
|
617
|
+
fields: Object.freeze({ ...this._fieldTotals }),
|
|
618
|
+
terms: this._postings.size,
|
|
619
|
+
tools: this.size
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
search(query, options = {}) {
|
|
623
|
+
if (typeof query !== "string") fail("ERR_TOOL_QUERY", "Search query must be a string");
|
|
624
|
+
if (query.length > this._options.maxQueryLength) {
|
|
625
|
+
fail("ERR_TOOL_QUERY_SIZE", `Search query exceeds maxQueryLength (${this._options.maxQueryLength})`);
|
|
626
|
+
}
|
|
627
|
+
const limit = positiveInteger(options.limit, 5, 1e3, "limit");
|
|
628
|
+
const minScore = finiteNumber(options.minScore, 0.01, 0, Number.MAX_SAFE_INTEGER, "minScore");
|
|
629
|
+
const queryTokens = this._tokenize(query).concat(queryAcronyms(query));
|
|
630
|
+
if (queryTokens.length === 0 || this.size === 0) return [];
|
|
631
|
+
const queryCounts = countTokens(queryTokens);
|
|
632
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
633
|
+
const matches = compileFilters(options);
|
|
634
|
+
const filterCache = /* @__PURE__ */ new Map();
|
|
635
|
+
for (const [queryToken, queryFrequency] of queryCounts) {
|
|
636
|
+
const expansions = this._expand(queryToken);
|
|
637
|
+
const tokenCandidates = /* @__PURE__ */ new Map();
|
|
638
|
+
for (const [term, expansionWeight] of expansions) {
|
|
639
|
+
const posting = this._postings.get(term);
|
|
640
|
+
if (!posting) continue;
|
|
641
|
+
const documentFrequency = posting.size;
|
|
642
|
+
const inverseFrequency = Math.log(1 + (this.size - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
643
|
+
for (const [id, frequencies] of posting) {
|
|
644
|
+
const document = this._documents.get(id);
|
|
645
|
+
if (!document) continue;
|
|
646
|
+
let allowed = filterCache.get(id);
|
|
647
|
+
if (allowed === void 0) {
|
|
648
|
+
allowed = matches(document.record);
|
|
649
|
+
filterCache.set(id, allowed);
|
|
650
|
+
}
|
|
651
|
+
if (!allowed) continue;
|
|
652
|
+
let weightedFrequency = 0;
|
|
653
|
+
const fields = [];
|
|
654
|
+
for (const field of FIELD_NAMES) {
|
|
655
|
+
const frequency = frequencies[field] || 0;
|
|
656
|
+
if (frequency === 0 || this._options.fieldWeights[field] === 0) continue;
|
|
657
|
+
const averageLength = this._fieldTotals[field] / Math.max(1, this.size);
|
|
658
|
+
const normalization = 1 - this._options.b + this._options.b * (document.fieldLengths[field] / averageLength);
|
|
659
|
+
weightedFrequency += this._options.fieldWeights[field] * frequency / normalization;
|
|
660
|
+
fields.push(field);
|
|
661
|
+
}
|
|
662
|
+
if (weightedFrequency === 0) continue;
|
|
663
|
+
const saturation = weightedFrequency * (this._options.k1 + 1) / (weightedFrequency + this._options.k1);
|
|
664
|
+
const contribution = inverseFrequency * saturation * expansionWeight * (1 + Math.log(queryFrequency));
|
|
665
|
+
let tokenState = tokenCandidates.get(id);
|
|
666
|
+
if (!tokenState) {
|
|
667
|
+
tokenState = { contribution: 0, fields: /* @__PURE__ */ new Set(), terms: /* @__PURE__ */ new Set() };
|
|
668
|
+
tokenCandidates.set(id, tokenState);
|
|
669
|
+
}
|
|
670
|
+
if (contribution > tokenState.contribution) {
|
|
671
|
+
tokenState.contribution = contribution;
|
|
672
|
+
tokenState.fields = new Set(fields);
|
|
673
|
+
} else if (contribution === tokenState.contribution) {
|
|
674
|
+
for (const field of fields) tokenState.fields.add(field);
|
|
675
|
+
}
|
|
676
|
+
tokenState.terms.add(term);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
for (const [id, tokenState] of tokenCandidates) {
|
|
680
|
+
let state = candidates.get(id);
|
|
681
|
+
if (!state) {
|
|
682
|
+
state = {
|
|
683
|
+
fields: /* @__PURE__ */ new Set(),
|
|
684
|
+
queryTerms: /* @__PURE__ */ new Set(),
|
|
685
|
+
score: 0,
|
|
686
|
+
terms: /* @__PURE__ */ new Set()
|
|
687
|
+
};
|
|
688
|
+
candidates.set(id, state);
|
|
689
|
+
}
|
|
690
|
+
state.score += tokenState.contribution;
|
|
691
|
+
state.queryTerms.add(queryToken);
|
|
692
|
+
for (const term of tokenState.terms) state.terms.add(term);
|
|
693
|
+
for (const field of tokenState.fields) state.fields.add(field);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const normalizedQuery = normalizeIdentifier(query);
|
|
697
|
+
const ranked = [];
|
|
698
|
+
for (const [id, state] of candidates) {
|
|
699
|
+
const document = this._documents.get(id);
|
|
700
|
+
let score = state.score;
|
|
701
|
+
if (normalizedQuery && document.identifiers.has(normalizedQuery)) score += 12;
|
|
702
|
+
else if (normalizedQuery && document.identifiersText.includes(normalizedQuery)) score += 2;
|
|
703
|
+
for (const queryToken of queryCounts.keys()) {
|
|
704
|
+
if (document.nameTokens.has(queryToken)) score += 1.5;
|
|
705
|
+
if (document.namespaceTokens.has(queryToken)) score += 2;
|
|
706
|
+
}
|
|
707
|
+
const coverage = state.queryTerms.size / queryCounts.size;
|
|
708
|
+
score = score * (0.65 + 0.35 * coverage) + coverage;
|
|
709
|
+
if (score < minScore) continue;
|
|
710
|
+
ranked.push({ document, score, state, coverage });
|
|
711
|
+
}
|
|
712
|
+
ranked.sort((left, right) => right.score - left.score || left.document.record.name.localeCompare(right.document.record.name) || left.document.record.id.localeCompare(right.document.record.id));
|
|
713
|
+
const results = [];
|
|
714
|
+
let tokens = 0;
|
|
715
|
+
const tokenBudget = options.maxEstimatedTokens === void 0 ? Infinity : finiteNumber(options.maxEstimatedTokens, Infinity, 1, Number.MAX_SAFE_INTEGER, "maxEstimatedTokens");
|
|
716
|
+
for (const item of ranked) {
|
|
717
|
+
if (results.length >= limit) break;
|
|
718
|
+
if (tokens + item.document.estimatedTokens > tokenBudget) continue;
|
|
719
|
+
results.push(publicMatch(
|
|
720
|
+
item.document,
|
|
721
|
+
item.score,
|
|
722
|
+
item.state.terms,
|
|
723
|
+
item.state.fields,
|
|
724
|
+
Number(item.coverage.toFixed(6))
|
|
725
|
+
));
|
|
726
|
+
tokens += item.document.estimatedTokens;
|
|
727
|
+
}
|
|
728
|
+
return results;
|
|
729
|
+
}
|
|
730
|
+
select(query, options = {}) {
|
|
731
|
+
return this.search(query, options).map((match) => match.tool);
|
|
732
|
+
}
|
|
733
|
+
route(query, options = {}) {
|
|
734
|
+
const maxTools = positiveInteger(options.maxTools, 5, 1e3, "maxTools");
|
|
735
|
+
const tokenBudget = options.maxEstimatedTokens === void 0 ? Infinity : finiteNumber(options.maxEstimatedTokens, Infinity, 1, Number.MAX_SAFE_INTEGER, "maxEstimatedTokens");
|
|
736
|
+
const selected = [];
|
|
737
|
+
const selectedIds = /* @__PURE__ */ new Set();
|
|
738
|
+
let estimatedTokens = 0;
|
|
739
|
+
let budgetExceeded = false;
|
|
740
|
+
const pinned = options.pinned === void 0 ? [] : typeof options.pinned === "string" ? [options.pinned] : options.pinned;
|
|
741
|
+
if (!Array.isArray(pinned)) fail("ERR_TOOL_ROUTER_OPTION", "pinned must be a string or array of strings");
|
|
742
|
+
for (const id of pinned) {
|
|
743
|
+
if (selected.length >= maxTools) break;
|
|
744
|
+
const document = this._resolveDocument(id);
|
|
745
|
+
if (!document || selectedIds.has(document.record.id)) continue;
|
|
746
|
+
selected.push(publicMatch(document, null, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), 0, true));
|
|
747
|
+
selectedIds.add(document.record.id);
|
|
748
|
+
estimatedTokens += document.estimatedTokens;
|
|
749
|
+
if (estimatedTokens > tokenBudget) budgetExceeded = true;
|
|
750
|
+
}
|
|
751
|
+
const candidates = this.search(query, {
|
|
752
|
+
...options,
|
|
753
|
+
limit: Math.min(1e3, Math.max(maxTools, maxTools * 4)),
|
|
754
|
+
maxEstimatedTokens: void 0
|
|
755
|
+
});
|
|
756
|
+
for (const match of candidates) {
|
|
757
|
+
if (selected.length >= maxTools) break;
|
|
758
|
+
if (selectedIds.has(match.id)) continue;
|
|
759
|
+
if (estimatedTokens + match.estimatedTokens > tokenBudget) continue;
|
|
760
|
+
selected.push(match);
|
|
761
|
+
selectedIds.add(match.id);
|
|
762
|
+
estimatedTokens += match.estimatedTokens;
|
|
763
|
+
}
|
|
764
|
+
const fallback = options.fallback === void 0 ? "none" : options.fallback;
|
|
765
|
+
if (!["none", "first", "all"].includes(fallback)) {
|
|
766
|
+
fail("ERR_TOOL_ROUTER_OPTION", "fallback must be none, first, or all");
|
|
767
|
+
}
|
|
768
|
+
if (selected.length === 0 && fallback !== "none") {
|
|
769
|
+
const matches = compileFilters(options);
|
|
770
|
+
const documents = Array.from(this._documents.values()).filter((document) => matches(document.record)).sort((left, right) => left.record.id.localeCompare(right.record.id));
|
|
771
|
+
const fallbackLimit = fallback === "first" ? 1 : maxTools;
|
|
772
|
+
for (const document of documents) {
|
|
773
|
+
if (selected.length >= fallbackLimit) break;
|
|
774
|
+
if (estimatedTokens + document.estimatedTokens > tokenBudget) continue;
|
|
775
|
+
selected.push(publicMatch(document, null, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), 0));
|
|
776
|
+
selectedIds.add(document.record.id);
|
|
777
|
+
estimatedTokens += document.estimatedTokens;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const reduction = this._estimatedTokens === 0 ? 0 : Math.max(0, 1 - estimatedTokens / this._estimatedTokens);
|
|
781
|
+
return Object.freeze({
|
|
782
|
+
budgetExceeded,
|
|
783
|
+
catalogEstimatedTokens: this._estimatedTokens,
|
|
784
|
+
catalogSize: this.size,
|
|
785
|
+
estimatedTokens,
|
|
786
|
+
matches: Object.freeze(selected),
|
|
787
|
+
records: Object.freeze(selected.map((match) => match.record)),
|
|
788
|
+
selectedCount: selected.length,
|
|
789
|
+
tokenReduction: Number(reduction.toFixed(6)),
|
|
790
|
+
tools: Object.freeze(selected.map((match) => match.tool))
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
_prepare(record) {
|
|
794
|
+
const schemaText = collectSchemaText(record.inputSchema, this._options);
|
|
795
|
+
const fields = {
|
|
796
|
+
aliases: record.aliases.join(" "),
|
|
797
|
+
description: record.description,
|
|
798
|
+
name: nameSearchText(record.name),
|
|
799
|
+
namespace: record.namespace,
|
|
800
|
+
schema: schemaText,
|
|
801
|
+
tags: record.tags.join(" ")
|
|
802
|
+
};
|
|
803
|
+
const fieldTokens = /* @__PURE__ */ Object.create(null);
|
|
804
|
+
const fieldLengths = /* @__PURE__ */ Object.create(null);
|
|
805
|
+
const identifiers = /* @__PURE__ */ new Set([normalizeIdentifier(record.name)]);
|
|
806
|
+
for (const alias of record.aliases) identifiers.add(normalizeIdentifier(alias));
|
|
807
|
+
if (record.namespace) identifiers.add(normalizeIdentifier(`${record.namespace} ${record.name}`));
|
|
808
|
+
identifiers.delete("");
|
|
809
|
+
let textLength = 0;
|
|
810
|
+
for (const field of FIELD_NAMES) {
|
|
811
|
+
textLength += fields[field].length;
|
|
812
|
+
if (fields[field].length > this._options.maxTextLength || textLength > this._options.maxTextLength * 2) {
|
|
813
|
+
fail("ERR_TOOL_TEXT_SIZE", `Tool ${record.id} exceeds maxTextLength (${this._options.maxTextLength})`);
|
|
814
|
+
}
|
|
815
|
+
const tokens = this._tokenize(fields[field]);
|
|
816
|
+
fieldTokens[field] = countTokens(tokens);
|
|
817
|
+
fieldLengths[field] = tokens.length;
|
|
818
|
+
}
|
|
819
|
+
for (const acronym of nameAcronyms(record.name)) {
|
|
820
|
+
const term = `acr:${acronym}`;
|
|
821
|
+
fieldTokens.name.set(term, (fieldTokens.name.get(term) || 0) + 1);
|
|
822
|
+
fieldLengths.name += 1;
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
estimatedTokens: estimateJsonTokens(record.original),
|
|
826
|
+
fieldLengths,
|
|
827
|
+
fieldTokens,
|
|
828
|
+
identifiers,
|
|
829
|
+
identifiersText: Array.from(identifiers).join(" "),
|
|
830
|
+
nameTokens: new Set(this._tokenize(record.name)),
|
|
831
|
+
namespaceTokens: new Set(this._tokenize(record.namespace)),
|
|
832
|
+
record
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
_tokenize(text) {
|
|
836
|
+
const result = this._options.tokenizer(text);
|
|
837
|
+
if (!Array.isArray(result)) fail("ERR_TOOL_TOKENIZER", "tokenizer must return an array of strings");
|
|
838
|
+
const tokens = [];
|
|
839
|
+
for (const token of result) {
|
|
840
|
+
if (typeof token !== "string") fail("ERR_TOOL_TOKENIZER", "tokenizer results must be strings");
|
|
841
|
+
if (token.length > 0 && token.length <= 256) tokens.push(token);
|
|
842
|
+
}
|
|
843
|
+
return tokens;
|
|
844
|
+
}
|
|
845
|
+
_addDocument(document) {
|
|
846
|
+
this._documents.set(document.record.id, document);
|
|
847
|
+
this._estimatedTokens += document.estimatedTokens;
|
|
848
|
+
for (const field of FIELD_NAMES) {
|
|
849
|
+
this._fieldTotals[field] += document.fieldLengths[field];
|
|
850
|
+
for (const [term, frequency] of document.fieldTokens[field]) {
|
|
851
|
+
let posting = this._postings.get(term);
|
|
852
|
+
const isNewTerm = !posting;
|
|
853
|
+
if (!posting) {
|
|
854
|
+
posting = /* @__PURE__ */ new Map();
|
|
855
|
+
this._postings.set(term, posting);
|
|
856
|
+
}
|
|
857
|
+
let frequencies = posting.get(document.record.id);
|
|
858
|
+
if (!frequencies) {
|
|
859
|
+
frequencies = /* @__PURE__ */ Object.create(null);
|
|
860
|
+
posting.set(document.record.id, frequencies);
|
|
861
|
+
}
|
|
862
|
+
frequencies[field] = frequency;
|
|
863
|
+
if (isNewTerm) this._addLexiconTerm(term);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
_removeDocument(id) {
|
|
868
|
+
const document = this._documents.get(id);
|
|
869
|
+
if (!document) return false;
|
|
870
|
+
this._documents.delete(id);
|
|
871
|
+
this._estimatedTokens -= document.estimatedTokens;
|
|
872
|
+
for (const field of FIELD_NAMES) {
|
|
873
|
+
this._fieldTotals[field] -= document.fieldLengths[field];
|
|
874
|
+
for (const term of document.fieldTokens[field].keys()) {
|
|
875
|
+
const posting = this._postings.get(term);
|
|
876
|
+
if (!posting) continue;
|
|
877
|
+
posting.delete(id);
|
|
878
|
+
if (posting.size === 0) {
|
|
879
|
+
this._postings.delete(term);
|
|
880
|
+
this._removeLexiconTerm(term);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
_addLexiconTerm(term) {
|
|
887
|
+
const maximum = Math.min(8, term.length);
|
|
888
|
+
for (let length = 2; length <= maximum; length++) {
|
|
889
|
+
const prefix = term.slice(0, length);
|
|
890
|
+
let terms = this._prefixes.get(prefix);
|
|
891
|
+
if (!terms) {
|
|
892
|
+
terms = /* @__PURE__ */ new Set();
|
|
893
|
+
this._prefixes.set(prefix, terms);
|
|
894
|
+
}
|
|
895
|
+
terms.add(term);
|
|
896
|
+
}
|
|
897
|
+
for (const trigram of new Set(tokenTrigrams(term))) {
|
|
898
|
+
let terms = this._trigrams.get(trigram);
|
|
899
|
+
if (!terms) {
|
|
900
|
+
terms = /* @__PURE__ */ new Set();
|
|
901
|
+
this._trigrams.set(trigram, terms);
|
|
902
|
+
}
|
|
903
|
+
terms.add(term);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
_removeLexiconTerm(term) {
|
|
907
|
+
const maximum = Math.min(8, term.length);
|
|
908
|
+
for (let length = 2; length <= maximum; length++) {
|
|
909
|
+
const prefix = term.slice(0, length);
|
|
910
|
+
const terms = this._prefixes.get(prefix);
|
|
911
|
+
if (!terms) continue;
|
|
912
|
+
terms.delete(term);
|
|
913
|
+
if (terms.size === 0) this._prefixes.delete(prefix);
|
|
914
|
+
}
|
|
915
|
+
for (const trigram of new Set(tokenTrigrams(term))) {
|
|
916
|
+
const terms = this._trigrams.get(trigram);
|
|
917
|
+
if (!terms) continue;
|
|
918
|
+
terms.delete(term);
|
|
919
|
+
if (terms.size === 0) this._trigrams.delete(trigram);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
_expand(queryToken) {
|
|
923
|
+
const expanded = /* @__PURE__ */ new Map();
|
|
924
|
+
this._addLiteralExpansions(expanded, queryToken, 1);
|
|
925
|
+
const synonyms = this._options.synonyms[queryToken];
|
|
926
|
+
if (synonyms) {
|
|
927
|
+
for (const synonym of synonyms) this._addLiteralExpansions(expanded, synonym, 0.72);
|
|
928
|
+
}
|
|
929
|
+
if (expanded.size > 0 || !this._options.fuzzy || queryToken.length < this._options.minFuzzyLength) {
|
|
930
|
+
return new Map(Array.from(expanded).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).slice(0, this._options.maxExpansions));
|
|
931
|
+
}
|
|
932
|
+
const queryTrigrams = new Set(tokenTrigrams(queryToken));
|
|
933
|
+
const overlap = /* @__PURE__ */ new Map();
|
|
934
|
+
for (const trigram of queryTrigrams) {
|
|
935
|
+
const terms = this._trigrams.get(trigram);
|
|
936
|
+
if (!terms) continue;
|
|
937
|
+
for (const term of terms) overlap.set(term, (overlap.get(term) || 0) + 1);
|
|
938
|
+
}
|
|
939
|
+
const candidates = [];
|
|
940
|
+
const maximumDistance = queryToken.length <= 5 ? 1 : 2;
|
|
941
|
+
for (const [term, shared] of overlap) {
|
|
942
|
+
const termTrigrams = new Set(tokenTrigrams(term));
|
|
943
|
+
const similarity = shared / (queryTrigrams.size + termTrigrams.size - shared);
|
|
944
|
+
if (similarity < 0.25) continue;
|
|
945
|
+
const distance = boundedEditDistance(queryToken, term, maximumDistance);
|
|
946
|
+
if (distance > maximumDistance) continue;
|
|
947
|
+
const editSimilarity = 1 - distance / Math.max(queryToken.length, term.length);
|
|
948
|
+
candidates.push({ term, weight: 0.55 + 0.25 * editSimilarity, similarity });
|
|
949
|
+
}
|
|
950
|
+
candidates.sort((left, right) => right.similarity - left.similarity || right.weight - left.weight || left.term.localeCompare(right.term));
|
|
951
|
+
for (const candidate of candidates.slice(0, this._options.maxExpansions)) {
|
|
952
|
+
expanded.set(candidate.term, candidate.weight);
|
|
953
|
+
}
|
|
954
|
+
return expanded;
|
|
955
|
+
}
|
|
956
|
+
_addLiteralExpansions(expanded, token, weight) {
|
|
957
|
+
if (this._postings.has(token)) expanded.set(token, Math.max(weight, expanded.get(token) || 0));
|
|
958
|
+
if (token.length < 3) return;
|
|
959
|
+
const prefix = token.slice(0, Math.min(8, token.length));
|
|
960
|
+
const prefixTerms = this._prefixes.get(prefix);
|
|
961
|
+
if (!prefixTerms) return;
|
|
962
|
+
const ordered = Array.from(prefixTerms).filter((term) => term.startsWith(token) && term !== token).sort((left, right) => Math.abs(left.length - token.length) - Math.abs(right.length - token.length) || left.localeCompare(right));
|
|
963
|
+
for (const term of ordered.slice(0, this._options.maxExpansions)) {
|
|
964
|
+
expanded.set(term, Math.max(weight * 0.78, expanded.get(term) || 0));
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
_resolveDocument(idOrName) {
|
|
968
|
+
if (typeof idOrName !== "string") return void 0;
|
|
969
|
+
const exact = this._documents.get(idOrName);
|
|
970
|
+
if (exact) return exact;
|
|
971
|
+
let match;
|
|
972
|
+
for (const document of this._documents.values()) {
|
|
973
|
+
if (document.record.name !== idOrName) continue;
|
|
974
|
+
if (!match || document.record.id.localeCompare(match.record.id) < 0) match = document;
|
|
975
|
+
}
|
|
976
|
+
return match;
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
function createToolRouter(tools = [], options = {}) {
|
|
980
|
+
return new ToolRouter(tools, options);
|
|
981
|
+
}
|
|
982
|
+
function routeTools(tools, query, options = {}) {
|
|
983
|
+
const routerOptions = options.router || {};
|
|
984
|
+
const routeOptions = { ...options };
|
|
985
|
+
delete routeOptions.router;
|
|
986
|
+
return createToolRouter(tools, routerOptions).route(query, routeOptions);
|
|
987
|
+
}
|
|
988
|
+
function estimateToolTokens(tool) {
|
|
989
|
+
return estimateJsonTokens(tool);
|
|
990
|
+
}
|
|
991
|
+
function defineTool(tool) {
|
|
992
|
+
return normalizeTool(tool).original;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/search-tool.js
|
|
996
|
+
var SEARCH_SCHEMA = Object.freeze({
|
|
997
|
+
additionalProperties: false,
|
|
998
|
+
properties: {
|
|
999
|
+
limit: {
|
|
1000
|
+
description: "Maximum number of matching tool summaries to return.",
|
|
1001
|
+
maximum: 25,
|
|
1002
|
+
minimum: 1,
|
|
1003
|
+
type: "integer"
|
|
1004
|
+
},
|
|
1005
|
+
query: {
|
|
1006
|
+
description: "Capability, action, service, or resource needed for the task.",
|
|
1007
|
+
minLength: 1,
|
|
1008
|
+
type: "string"
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
required: ["query"],
|
|
1012
|
+
type: "object"
|
|
1013
|
+
});
|
|
1014
|
+
function definitionFor(target, name, description) {
|
|
1015
|
+
if (target === "openai-responses") {
|
|
1016
|
+
return { type: "function", name, description, parameters: SEARCH_SCHEMA };
|
|
1017
|
+
}
|
|
1018
|
+
if (target === "openai-chat") {
|
|
1019
|
+
return { type: "function", function: { name, description, parameters: SEARCH_SCHEMA } };
|
|
1020
|
+
}
|
|
1021
|
+
if (target === "anthropic") return { name, description, input_schema: SEARCH_SCHEMA };
|
|
1022
|
+
if (target === "gemini") return { name, description, parameters: SEARCH_SCHEMA };
|
|
1023
|
+
if (target === "mcp") return { name, description, inputSchema: SEARCH_SCHEMA };
|
|
1024
|
+
if (target === "canonical") return { name, description, inputSchema: SEARCH_SCHEMA };
|
|
1025
|
+
fail("ERR_TOOL_FORMAT", `Unsupported search tool target: ${String(target)}`);
|
|
1026
|
+
}
|
|
1027
|
+
function createToolSearch(router, options = {}) {
|
|
1028
|
+
if (!router || typeof router.search !== "function") {
|
|
1029
|
+
fail("ERR_TOOL_ROUTER", "createToolSearch requires a ToolRouter instance");
|
|
1030
|
+
}
|
|
1031
|
+
const target = options.target === void 0 ? "canonical" : options.target;
|
|
1032
|
+
const name = options.name === void 0 ? "search_tools" : options.name;
|
|
1033
|
+
const description = options.description === void 0 ? "Find the smallest set of available tools relevant to a capability or task." : options.description;
|
|
1034
|
+
if (typeof name !== "string" || name.length === 0) fail("ERR_TOOL_NAME", "Search tool name must be a non-empty string");
|
|
1035
|
+
if (typeof description !== "string") fail("ERR_TOOL_DEFINITION", "Search tool description must be a string");
|
|
1036
|
+
const defaultLimit = options.limit === void 0 ? 5 : options.limit;
|
|
1037
|
+
if (!Number.isInteger(defaultLimit) || defaultLimit < 1 || defaultLimit > 25) {
|
|
1038
|
+
fail("ERR_TOOL_ROUTER_OPTION", "Search tool limit must be an integer between 1 and 25");
|
|
1039
|
+
}
|
|
1040
|
+
const definition = definitionFor(target, name, description);
|
|
1041
|
+
return Object.freeze({
|
|
1042
|
+
definition,
|
|
1043
|
+
execute(input) {
|
|
1044
|
+
if (!input || typeof input !== "object" || typeof input.query !== "string" || input.query.length === 0) {
|
|
1045
|
+
fail("ERR_TOOL_QUERY", "Search tool input.query must be a non-empty string");
|
|
1046
|
+
}
|
|
1047
|
+
const limit = input.limit === void 0 ? defaultLimit : input.limit;
|
|
1048
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 25) {
|
|
1049
|
+
fail("ERR_TOOL_QUERY", "Search tool input.limit must be an integer between 1 and 25");
|
|
1050
|
+
}
|
|
1051
|
+
const matches = router.search(input.query, { limit });
|
|
1052
|
+
return Object.freeze({
|
|
1053
|
+
catalogSize: router.size,
|
|
1054
|
+
query: input.query,
|
|
1055
|
+
tools: Object.freeze(matches.map((match) => Object.freeze({
|
|
1056
|
+
description: match.record.description,
|
|
1057
|
+
id: match.id,
|
|
1058
|
+
name: match.name,
|
|
1059
|
+
namespace: match.record.namespace || void 0,
|
|
1060
|
+
score: match.score,
|
|
1061
|
+
tags: match.record.tags
|
|
1062
|
+
})))
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/index.js
|
|
1069
|
+
var index_default = Object.freeze({
|
|
1070
|
+
TOOL_FORMATS,
|
|
1071
|
+
ToolRouter,
|
|
1072
|
+
ToolRouterError,
|
|
1073
|
+
createToolRouter,
|
|
1074
|
+
createToolSearch,
|
|
1075
|
+
defineTool,
|
|
1076
|
+
detectToolFormat,
|
|
1077
|
+
estimateToolTokens,
|
|
1078
|
+
normalizeIdentifier,
|
|
1079
|
+
normalizeText,
|
|
1080
|
+
normalizeTool,
|
|
1081
|
+
normalizeTools,
|
|
1082
|
+
routeTools,
|
|
1083
|
+
tokenize
|
|
1084
|
+
});
|
|
1085
|
+
export {
|
|
1086
|
+
TOOL_FORMATS,
|
|
1087
|
+
ToolRouter,
|
|
1088
|
+
ToolRouterError,
|
|
1089
|
+
createToolRouter,
|
|
1090
|
+
createToolSearch,
|
|
1091
|
+
index_default as default,
|
|
1092
|
+
defineTool,
|
|
1093
|
+
detectToolFormat,
|
|
1094
|
+
estimateToolTokens,
|
|
1095
|
+
normalizeIdentifier,
|
|
1096
|
+
normalizeText,
|
|
1097
|
+
normalizeTool,
|
|
1098
|
+
normalizeTools,
|
|
1099
|
+
routeTools,
|
|
1100
|
+
tokenize
|
|
1101
|
+
};
|
|
1102
|
+
//# sourceMappingURL=index.js.map
|