@tangleai/agents 0.21.1 → 0.25.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 +45 -0
- package/README.md +7 -6
- package/package.json +6 -6
- package/src/agent.d.ts +31 -33
- package/src/agent.js +522 -669
- package/src/index.d.ts +12 -11
- package/src/index.js +7 -12
- package/src/program-result.d.ts +7 -29
- package/src/program-result.js +16 -40
- package/src/program-session.d.ts +7 -13
- package/src/program-session.js +130 -108
- package/src/program-shape.d.ts +17 -17
- package/src/program-shape.js +47 -40
- package/src/program.d.ts +140 -108
- package/src/program.js +637 -712
- package/src/recursive.d.ts +65 -37
- package/src/recursive.js +223 -263
- package/src/refine.d.ts +49 -15
- package/src/refine.js +396 -445
- package/src/schemas/program.d.ts +25 -25
- package/src/schemas/program.js +88 -104
- package/src/toolbox.d.ts +27 -26
- package/src/toolbox.js +90 -124
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
|
170
|
-
* @param
|
|
171
|
-
* @param
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
}
|