@jxsuite/ai 0.1.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/src/tools.ts ADDED
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Tools.js — Tool registry infrastructure
3
+ *
4
+ * Provider-agnostic base classes for defining and registering tools that an LLM
5
+ * can call. Tools have JSON Schema parameter definitions and execute functions.
6
+ * The registry validates arguments before execution.
7
+ *
8
+ * @license MIT
9
+ * @module @jxsuite/ai/tools
10
+ */
11
+
12
+ export interface ToolResult {
13
+ success: boolean;
14
+ data?: unknown;
15
+ error?: string;
16
+ summary?: string;
17
+ }
18
+
19
+ /**
20
+ * JSON Schema fragment describing a tool's parameters. Only the subset the registry's lightweight
21
+ * validator inspects is modeled; arbitrary extra keywords are permitted.
22
+ */
23
+ export interface JSONSchema {
24
+ type?: string;
25
+ properties?: Record<string, JSONSchema>;
26
+ required?: string[];
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export interface ToolDefinition {
31
+ name: string;
32
+ description: string;
33
+ parameters: JSONSchema;
34
+ strict: boolean;
35
+ llmStrict: boolean;
36
+ execute: (args: object) => Promise<ToolResult> | ToolResult;
37
+ }
38
+
39
+ export interface ToolRegistry {
40
+ register: (tool: ToolDefinition) => void;
41
+ list: () => ToolDefinition[];
42
+ listForLLM: () => object[];
43
+ validate: (toolName: string, args: object) => { valid: boolean; errors?: string[] };
44
+ execute: (toolName: string, args: object) => Promise<ToolResult>;
45
+ getDefinition: (toolName: string) => ToolDefinition | undefined;
46
+ }
47
+
48
+ // ─── ToolResult ──────────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Create a successful tool result.
52
+ *
53
+ * @param {unknown} data - Result data
54
+ * @param {string} [summary] - Human-readable summary
55
+ * @returns {ToolResult}
56
+ */
57
+ export function toolSuccess(data: unknown, summary?: string): ToolResult {
58
+ return { success: true, data, summary } as ToolResult;
59
+ }
60
+
61
+ /**
62
+ * Create a failed tool result.
63
+ *
64
+ * @param {string} error - Error message
65
+ * @returns {ToolResult}
66
+ */
67
+ export function toolError(error: string): ToolResult {
68
+ return { success: false, error };
69
+ }
70
+
71
+ // ─── ToolDefinition ──────────────────────────────────────────────────────────
72
+
73
+ /**
74
+ * Create a tool definition.
75
+ *
76
+ * @param {object} opts
77
+ * @param {string} opts.name - Unique tool name (lowercase, underscores for spaces)
78
+ * @param {string} opts.description - What the tool does (used by the LLM to decide when to call it)
79
+ * @param {object} opts.parameters - JSON Schema for the tool's arguments
80
+ * @param {boolean} [opts.strict] - Whether the registry enforces its own argument validation
81
+ * (default true). Distinct from `llmStrict`.
82
+ * @param {boolean} [opts.llmStrict] - Whether to send OpenAI `strict: true` in the function schema
83
+ * (default false). Only enable if the parameter schema is OpenAI-strict-compliant
84
+ * (additionalProperties:false everywhere, all properties in `required`, every property typed).
85
+ * @param {(args: object) => Promise<ToolResult> | ToolResult} opts.execute - The tool
86
+ * implementation
87
+ * @returns {ToolDefinition}
88
+ */
89
+ export function createToolDefinition({
90
+ name,
91
+ description,
92
+ parameters,
93
+ strict = true,
94
+ llmStrict = false,
95
+ execute,
96
+ }: {
97
+ name: string;
98
+ description: string;
99
+ parameters: JSONSchema;
100
+ strict?: boolean;
101
+ llmStrict?: boolean;
102
+ execute: (args: object) => Promise<ToolResult> | ToolResult;
103
+ }): ToolDefinition {
104
+ return { name, description, parameters, strict, llmStrict, execute };
105
+ }
106
+
107
+ // ─── ToolRegistry ────────────────────────────────────────────────────────────
108
+
109
+ /**
110
+ * Create a new tool registry.
111
+ *
112
+ * @returns {ToolRegistry}
113
+ */
114
+ export function createToolRegistry() {
115
+ const _tools = new Map<string, ToolDefinition>();
116
+
117
+ return {
118
+ /**
119
+ * Register a tool definition.
120
+ *
121
+ * @param {ToolDefinition} tool
122
+ */
123
+ register(tool: ToolDefinition) {
124
+ if (_tools.has(tool.name)) {
125
+ console.warn(`Tool "${tool.name}" is being re-registered.`);
126
+ }
127
+ _tools.set(tool.name, tool);
128
+ },
129
+
130
+ /**
131
+ * List all registered tool definitions.
132
+ *
133
+ * @returns {ToolDefinition[]}
134
+ */
135
+ list() {
136
+ return [..._tools.values()];
137
+ },
138
+
139
+ /**
140
+ * List tools in OpenAI function-calling format.
141
+ *
142
+ * @returns {object[]}
143
+ */
144
+ listForLLM() {
145
+ return [..._tools.values()].map((t) => ({
146
+ type: "function",
147
+ function: {
148
+ name: t.name,
149
+ description: t.description,
150
+ parameters: t.parameters,
151
+ // OpenAI strict mode is opt-in per tool via `llmStrict`, NOT the `strict` field — that one
152
+ // Drives the registry's own argument validation below. They are different concerns:
153
+ // OpenAI strict requires every object to set additionalProperties:false and list all
154
+ // Properties in `required`, which the Jx tools deliberately don't (e.g. set_property's
155
+ // `value` is polymorphic and optional). GPT-5.x rejects the schema otherwise.
156
+ ...(t.llmStrict === true ? { strict: true } : {}),
157
+ },
158
+ }));
159
+ },
160
+
161
+ /**
162
+ * Get a single tool definition by name.
163
+ *
164
+ * @param {string} toolName
165
+ * @returns {ToolDefinition | undefined}
166
+ */
167
+ getDefinition(toolName: string) {
168
+ return _tools.get(toolName);
169
+ },
170
+
171
+ /**
172
+ * Validate tool arguments against the tool's JSON Schema.
173
+ *
174
+ * This is a lightweight structural check — it verifies required properties exist and property
175
+ * types match. Full JSON Schema validation (pattern, minimum, maximum, enum, etc.) can be
176
+ * layered on with a proper validator library if needed.
177
+ *
178
+ * @param {string} toolName
179
+ * @param {object} args
180
+ * @returns {{ valid: boolean; errors?: string[] }}
181
+ */
182
+ validate(toolName: string, args: object) {
183
+ const tool = _tools.get(toolName);
184
+ if (!tool) {
185
+ return { valid: false, errors: [`Unknown tool: "${toolName}"`] };
186
+ }
187
+
188
+ if (!tool.strict) {
189
+ return { valid: true };
190
+ }
191
+
192
+ const schema = tool.parameters;
193
+ if (!schema || !schema.properties) {
194
+ return { valid: true };
195
+ }
196
+
197
+ const errors: string[] = [];
198
+ const props = schema.properties;
199
+ const required = schema.required || [];
200
+ const argRecord = args as Record<string, unknown>;
201
+
202
+ // Check required properties
203
+ for (const key of required) {
204
+ if (!(key in args) || argRecord[key] === undefined || argRecord[key] === null) {
205
+ errors.push(`Missing required argument: "${key}"`);
206
+ }
207
+ }
208
+
209
+ // Check property types (basic)
210
+ for (const [key, value] of Object.entries(argRecord)) {
211
+ const propSchema = props[key];
212
+ if (!propSchema) {
213
+ // Unknown property — warn but don't fail (LLM might send extra)
214
+ continue;
215
+ }
216
+
217
+ const expectedType = propSchema.type;
218
+ if (!expectedType) {
219
+ continue;
220
+ }
221
+
222
+ const actualType = Array.isArray(value) ? "array" : typeof value;
223
+
224
+ // Number / integer type validation with string coercion
225
+ if (expectedType === "number" || expectedType === "integer") {
226
+ if (actualType === "number") {
227
+ continue;
228
+ }
229
+ if (actualType === "string") {
230
+ if (!Number.isNaN(Number(value))) {
231
+ continue;
232
+ }
233
+ errors.push(`Argument "${key}" should be a number, got non-numeric string "${value}"`);
234
+ continue;
235
+ }
236
+ errors.push(`Argument "${key}" should be a number, got ${actualType}`);
237
+ continue;
238
+ }
239
+
240
+ if (expectedType === "string" && actualType !== "string") {
241
+ errors.push(`Argument "${key}" should be a string, got ${actualType}`);
242
+ } else if (expectedType === "boolean" && actualType !== "boolean") {
243
+ errors.push(`Argument "${key}" should be a boolean, got ${actualType}`);
244
+ } else if (expectedType === "array" && actualType !== "array") {
245
+ errors.push(`Argument "${key}" should be an array, got ${actualType}`);
246
+ } else if (expectedType === "object" && actualType !== "object") {
247
+ errors.push(`Argument "${key}" should be an object, got ${actualType}`);
248
+ }
249
+ }
250
+
251
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
252
+ },
253
+
254
+ /**
255
+ * Execute a tool by name. Validates arguments before execution.
256
+ *
257
+ * @param {string} toolName
258
+ * @param {object} args
259
+ * @returns {Promise<ToolResult>}
260
+ */
261
+ async execute(toolName: string, args: object) {
262
+ const tool = _tools.get(toolName);
263
+ if (!tool) {
264
+ return toolError(`Unknown tool: "${toolName}"`);
265
+ }
266
+
267
+ // Validate
268
+ const validation = this.validate(toolName, args);
269
+ if (!validation.valid) {
270
+ return toolError(`Validation failed: ${(validation.errors || []).join("; ")}`);
271
+ }
272
+
273
+ try {
274
+ return await tool.execute(args);
275
+ } catch (error) {
276
+ return toolError(`Tool "${toolName}" execution error: ${(error as Error).message}`);
277
+ }
278
+ },
279
+ };
280
+ }