@tangleai/agents 0.21.1 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/toolbox.js CHANGED
@@ -1,4 +1,3 @@
1
- //@ts-check
2
1
  /**
3
2
  * The toolbox: a registry of tools an AI may call, each declared with
4
3
  * a JSON Schema `inputSchema` that Jaren itself compiles and enforces
@@ -16,163 +15,130 @@
16
15
  * JSON-encoded text for a structured property — results the calling
17
16
  * model can read and recover from.
18
17
  */
19
-
20
18
  import { JarenValidator } from '@jarenjs/validate';
21
19
  import { registerWebMcp } from '@jarenjs/contract/webmcp';
22
-
23
20
  import { checkOutcome } from '@jarenjs/core/check';
24
21
  import { invalidInput } from '@tangleai/models/check';
25
-
26
22
  /** Whether a property schema asks for structure (object or array). */
27
23
  function wantsStructure(schema) {
28
- const type = schema?.type;
29
- return type === 'object' || type === 'array'
30
- || (Array.isArray(type) && (type.includes('object') || type.includes('array')));
24
+ const type = schema?.type;
25
+ return type === 'object' || type === 'array'
26
+ || (Array.isArray(type) && (type.includes('object') || type.includes('array')));
31
27
  }
32
-
33
28
  /**
34
29
  * The top-level properties that wanted structure but arrived as
35
30
  * strings — after coercion failed, these are what went wrong.
36
- * @param {any} inputSchema
37
- * @param {any} input
38
- * @returns {string[]}
39
31
  */
40
32
  function stringStructureKeys(inputSchema, input) {
41
- const properties = inputSchema?.properties;
42
- if (properties == null || input === null || typeof input !== 'object') return [];
43
- return Object.entries(properties)
44
- .filter(([key, schema]) => wantsStructure(schema) && typeof input[key] === 'string')
45
- .map(([key]) => key);
33
+ const properties = inputSchema?.properties;
34
+ if (properties == null || input === null || typeof input !== 'object')
35
+ return [];
36
+ return Object.entries(properties)
37
+ .filter(([key, schema]) => wantsStructure(schema) && typeof input[key] === 'string')
38
+ .map(([key]) => key);
46
39
  }
47
-
48
40
  /**
49
41
  * A copy of `input` with string-valued top-level properties parsed as
50
42
  * JSON wherever the schema wants an object or array — or `null` when
51
43
  * nothing was coercible.
52
- * @param {any} inputSchema
53
- * @param {any} input
54
- * @returns {any | null}
55
44
  */
56
45
  function coerceStringArguments(inputSchema, input) {
57
- const properties = inputSchema?.properties;
58
- if (properties == null || input === null || typeof input !== 'object') return null;
59
- let coerced = null;
60
- for (const [key, schema] of Object.entries(properties)) {
61
- if (!wantsStructure(schema) || typeof input[key] !== 'string') continue;
62
- try {
63
- const parsed = JSON.parse(input[key]);
64
- if (parsed !== null && typeof parsed === 'object') {
65
- if (coerced === null) coerced = { ...input };
66
- coerced[key] = parsed;
67
- }
46
+ const properties = inputSchema?.properties;
47
+ if (properties == null || input === null || typeof input !== 'object')
48
+ return null;
49
+ let coerced = null;
50
+ for (const [key, schema] of Object.entries(properties)) {
51
+ if (!wantsStructure(schema) || typeof input[key] !== 'string')
52
+ continue;
53
+ try {
54
+ const parsed = JSON.parse(input[key]);
55
+ if (parsed !== null && typeof parsed === 'object') {
56
+ if (coerced === null)
57
+ coerced = { ...input };
58
+ coerced[key] = parsed;
59
+ }
60
+ }
61
+ catch {
62
+ // not JSON text: keep the original so validation reports it
63
+ }
68
64
  }
69
- catch {
70
- // not JSON text: keep the original so validation reports it
71
- }
72
- }
73
- return coerced;
65
+ return coerced;
74
66
  }
75
-
76
- /**
77
- * @typedef {Object} ToolDef
78
- * @property {string} name
79
- * @property {string} description
80
- * @property {any} inputSchema - JSON Schema for the arguments object
81
- * @property {(input: any) => any} execute - may return a value or a promise
82
- */
83
-
84
67
  /**
85
- * @param {{ validator?: any }} [options] - a shared JarenValidator, if
68
+ * @param [options] - a shared JarenValidator, if
86
69
  * the host already has one
87
- * @returns {{
88
- * add: (def: ToolDef) => void,
89
- * list: () => { name: string, description: string, inputSchema: any }[],
90
- * toFunctionTools: () => any[],
91
- * execute: (name: string, args: any) => any,
92
- * }}
93
70
  */
94
71
  export function createToolbox(options = {}) {
95
- const jaren = options.validator ?? new JarenValidator({ skipErrors: false, collectErrors: true });
96
- /** @type {Map<string, ToolDef & { check: (input: any) => any }>} */
97
- const tools = new Map();
98
-
99
- /** @param {ToolDef} def */
100
- function add(def) {
101
- tools.set(def.name, { ...def, check: jaren.compile(def.inputSchema) });
102
- }
103
-
104
- function list() {
105
- return [...tools.values()].map(({ name, description, inputSchema }) =>
106
- ({ name, description, inputSchema }));
107
- }
108
-
109
- function toFunctionTools() {
110
- return [...tools.values()].map(({ name, description, inputSchema }) => ({
111
- type: 'function',
112
- function: { name, description, parameters: inputSchema },
113
- }));
114
- }
115
-
116
- /**
117
- * Dispatch one call. Synchronous tools answer synchronously (WebMCP
118
- * hosts call `execute` directly); a promise-returning tool resolves
119
- * to its value with rejections folded into `{ error }`. A rejected
120
- * call answers `{ error, errors, inputSchema }` — the first
121
- * `MAX_INPUT_ERRORS` validation errors plus the schema to re-read —
122
- * with a `hint` added when a property wanting structure arrived as
123
- * unparseable JSON text.
124
- * @param {string} name
125
- * @param {any} args
126
- */
127
- function execute(name, args) {
128
- const tool = tools.get(name);
129
- if (tool === undefined) return { error: `unknown tool '${name}'` };
130
- let input = args ?? {};
131
- let outcome = checkOutcome(tool.check(input));
132
- if (!outcome.valid) {
133
- // models routinely JSON-encode nested arguments; when a property
134
- // wanted structure but arrived as parseable JSON text, validate
135
- // the parsed value instead — valid or not, its errors point into
136
- // the structure the model meant to send
137
- const coerced = coerceStringArguments(tool.inputSchema, input);
138
- if (coerced !== null) {
139
- input = coerced;
140
- outcome = checkOutcome(tool.check(coerced));
141
- }
72
+ const jaren = options.validator ?? new JarenValidator({ skipErrors: false, collectErrors: true });
73
+ const tools = new Map();
74
+ /** @param def */
75
+ function add(def) {
76
+ tools.set(def.name, { ...def, check: jaren.compile(def.inputSchema) });
142
77
  }
143
- if (!outcome.valid) {
144
- const stringly = stringStructureKeys(tool.inputSchema, input);
145
- return invalidInput(name, outcome, tool.inputSchema,
146
- stringly.length === 0 ? {} : {
147
- hint: `${stringly.map((k) => `'${k}'`).join(', ')} arrived as a JSON-encoded string that does not parse — pass a real JSON value, not quoted JSON text`,
148
- });
78
+ function list() {
79
+ return [...tools.values()].map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
149
80
  }
150
- try {
151
- const value = tool.execute(input);
152
- return typeof value?.then === 'function'
153
- ? value.then((resolved) => resolved, (/** @type {any} */ err) =>
154
- ({ error: err?.message ?? String(err) }))
155
- : value;
81
+ function toFunctionTools() {
82
+ return [...tools.values()].map(({ name, description, inputSchema }) => ({
83
+ type: 'function',
84
+ function: { name, description, parameters: inputSchema },
85
+ }));
156
86
  }
157
- catch (err) {
158
- return { error: /** @type {any} */ (err)?.message ?? String(err) };
87
+ /**
88
+ * Dispatch one call. Synchronous tools answer synchronously (WebMCP
89
+ * hosts call `execute` directly); a promise-returning tool resolves
90
+ * to its value with rejections folded into `{ error }`. A rejected
91
+ * call answers `{ error, errors, inputSchema }` — the first
92
+ * `MAX_INPUT_ERRORS` validation errors plus the schema to re-read —
93
+ * with a `hint` added when a property wanting structure arrived as
94
+ * unparseable JSON text.
95
+ */
96
+ function execute(name, args) {
97
+ const tool = tools.get(name);
98
+ if (tool === undefined)
99
+ return { error: `unknown tool '${name}'` };
100
+ let input = args ?? {};
101
+ let outcome = checkOutcome(tool.check(input));
102
+ if (!outcome.valid) {
103
+ // models routinely JSON-encode nested arguments; when a property
104
+ // wanted structure but arrived as parseable JSON text, validate
105
+ // the parsed value instead — valid or not, its errors point into
106
+ // the structure the model meant to send
107
+ const coerced = coerceStringArguments(tool.inputSchema, input);
108
+ if (coerced !== null) {
109
+ input = coerced;
110
+ outcome = checkOutcome(tool.check(coerced));
111
+ }
112
+ }
113
+ if (!outcome.valid) {
114
+ const stringly = stringStructureKeys(tool.inputSchema, input);
115
+ return invalidInput(name, outcome, tool.inputSchema, stringly.length === 0 ? {} : {
116
+ hint: `${stringly.map((k) => `'${k}'`).join(', ')} arrived as a JSON-encoded string that does not parse — pass a real JSON value, not quoted JSON text`,
117
+ });
118
+ }
119
+ try {
120
+ const value = tool.execute(input);
121
+ return typeof value?.then === 'function'
122
+ ? value.then((resolved) => resolved, (err) => ({ error: err?.message ?? String(err) }))
123
+ : value;
124
+ }
125
+ catch (err) {
126
+ return { error: err?.message ?? String(err) };
127
+ }
159
128
  }
160
- }
161
-
162
- return { add, list, toFunctionTools, execute };
129
+ return { add, list, toFunctionTools, execute };
163
130
  }
164
-
165
131
  /**
166
132
  * Publish validated tools through the shared browser adapter. Await `ready`
167
133
  * for completion and call `dispose` when the host leaves. The optional context
168
134
  * remains the second argument; undefined requests automatic discovery.
169
- * @param {ReturnType<typeof createToolbox>} toolbox
170
- * @param {unknown} [modelContext]
171
- * @param {(error: unknown) => void} [onError]
172
- * @param {Omit<import('@jarenjs/contract/webmcp').WebMcpOptions, 'context'|'onError'>} [options]
135
+ * @param [modelContext]
136
+ * @param [onError]
137
+ * @param [options]
173
138
  */
174
139
  export function registerModelContext(toolbox, modelContext, onError, options = {}) {
175
- return registerWebMcp(toolbox.list().map(def => ({ ...def,
176
- execute: (args) => toolbox.execute(def.name, args),
177
- })), { ...options, ...(modelContext === undefined ? {} : { context: modelContext }), onError });
140
+ return registerWebMcp(toolbox.list().map(def => ({
141
+ ...def,
142
+ execute: (args) => toolbox.execute(def.name, args),
143
+ })), { ...options, ...(modelContext === undefined ? {} : { context: modelContext }), onError });
178
144
  }