@tangleai/agents 0.21.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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +854 -0
- package/package.json +85 -0
- package/src/agent.d.ts +160 -0
- package/src/agent.js +1021 -0
- package/src/index.d.ts +11 -0
- package/src/index.js +13 -0
- package/src/program-result.d.ts +111 -0
- package/src/program-result.js +48 -0
- package/src/program-session.d.ts +48 -0
- package/src/program-session.js +121 -0
- package/src/program-shape.d.ts +21 -0
- package/src/program-shape.js +53 -0
- package/src/program.d.ts +244 -0
- package/src/program.js +940 -0
- package/src/recursive.d.ts +148 -0
- package/src/recursive.js +384 -0
- package/src/refine.d.ts +58 -0
- package/src/refine.js +599 -0
- package/src/schemas/program.d.ts +82 -0
- package/src/schemas/program.js +205 -0
- package/src/toolbox.d.ts +55 -0
- package/src/toolbox.js +178 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* The action language: what a model may say about the environment.
|
|
4
|
+
*
|
|
5
|
+
* The RLM paper's root model writes Python and an interpreter runs it.
|
|
6
|
+
* This package has no interpreter and will never have one — `eval` and
|
|
7
|
+
* `new Function` are forbidden by the house rules and a browser tab is
|
|
8
|
+
* the wrong place for a sandbox — so the model authors a **document**
|
|
9
|
+
* instead, and the document is put through the same two gates every
|
|
10
|
+
* generated Jaren program goes through: a schema that constrains
|
|
11
|
+
* decoding (the shape) and a compiler that runs before anything else
|
|
12
|
+
* does (the semantics).
|
|
13
|
+
*
|
|
14
|
+
* Read the two rules that shaped every line of this file:
|
|
15
|
+
*
|
|
16
|
+
* - **A step names slots; it never carries content** (D2). Every member
|
|
17
|
+
* below is an operation name, a binding name, a slot reference, a
|
|
18
|
+
* bounded instruction or a query document. There is no member a
|
|
19
|
+
* corpus can be poured into, and none can be added later without
|
|
20
|
+
* failing `test/ai/program.test.js` — the schema is walked and every
|
|
21
|
+
* string member must declare a `maxLength`. That is what makes "the
|
|
22
|
+
* program is constant-size whatever the corpus" a property of the
|
|
23
|
+
* grammar rather than a promise about how it will be used.
|
|
24
|
+
* - **`map` is the only step that calls a model.** One construct, one
|
|
25
|
+
* concurrency bound, one place to count spend. A grammar with
|
|
26
|
+
* sub-calls sprinkled through it cannot be bounded, and a runner over
|
|
27
|
+
* such a grammar cannot state what a program will cost before it runs
|
|
28
|
+
* it.
|
|
29
|
+
*
|
|
30
|
+
* Three shape decisions exist for the weak tier specifically (D8), and
|
|
31
|
+
* each one trades expressiveness for a decision the model does not have
|
|
32
|
+
* to make:
|
|
33
|
+
*
|
|
34
|
+
* - **Every step reads `from` and writes `as`.** Not `slot`/`in`/`over`
|
|
35
|
+
* per operation: one input member and one output member across the
|
|
36
|
+
* whole language, so the model picks the *operation* and never the
|
|
37
|
+
* spelling of its argument.
|
|
38
|
+
* - **Bindings are program-local names, not addresses.** The program
|
|
39
|
+
* says `as: "pieces"`; the runner resolves that to whatever the
|
|
40
|
+
* environment's derived addressing produced (HORIZON_06 owns
|
|
41
|
+
* addresses; a model that could write one could name a slot that
|
|
42
|
+
* cannot exist).
|
|
43
|
+
* - **`query` is left open unless a grammar is injected.** The seam is
|
|
44
|
+
* D3: with `@jarenjs/json`'s query grammar passed as a `ref` the
|
|
45
|
+
* shape is constrained too; without it the schema accepts any JSON
|
|
46
|
+
* value here and the compile gate is what refuses a bad one. A schema
|
|
47
|
+
* that hard-`$ref`'d a grammar this package may not import would make
|
|
48
|
+
* the whole language unusable with the seam empty.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/** Steps one program may have. A plan longer than this is a program
|
|
52
|
+
* that should have been two runs; it is also past the length a small
|
|
53
|
+
* model keeps coherent. */
|
|
54
|
+
export const MAX_STEPS = 12;
|
|
55
|
+
|
|
56
|
+
/** The whole document's character cap, enforced by the compiler. This
|
|
57
|
+
* is the constant in "constant-size root request": the program is one
|
|
58
|
+
* more thing the root carries, and it must not grow with the corpus. */
|
|
59
|
+
export const MAX_PROGRAM_CHARS = 4000;
|
|
60
|
+
|
|
61
|
+
/** A binding name: short, lowercase, unmistakable in an error message. */
|
|
62
|
+
export const NAME_PATTERN = '^[a-z][a-z0-9_]{0,31}$';
|
|
63
|
+
|
|
64
|
+
/** How long a slot reference may be — an address, never a payload. */
|
|
65
|
+
const SLOT_REF_MAX = 200;
|
|
66
|
+
|
|
67
|
+
/** How long a sub-call instruction may be. An instruction, not content:
|
|
68
|
+
* the content is the slot the sub-call is run over. */
|
|
69
|
+
const PROMPT_MAX = 1000;
|
|
70
|
+
|
|
71
|
+
/** How long a grep pattern may be. */
|
|
72
|
+
const PATTERN_MAX = 200;
|
|
73
|
+
|
|
74
|
+
/** The operations a program may name, in the order a plan uses them. */
|
|
75
|
+
export const PROGRAM_OPS = ['chunk', 'grep', 'select', 'stat', 'peek', 'map', 'reduce', 'answer'];
|
|
76
|
+
|
|
77
|
+
/** The one input member. See the file header for why it is not per-op. */
|
|
78
|
+
const FROM = {
|
|
79
|
+
type: 'string',
|
|
80
|
+
minLength: 1,
|
|
81
|
+
maxLength: SLOT_REF_MAX,
|
|
82
|
+
description: 'What this step reads: a name from an earlier step\'s "as", or a slot from the digest.',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** The one output member: the name later steps use to read this one.
|
|
86
|
+
* `maxLength` as well as the pattern, which already bounds it: the D2
|
|
87
|
+
* walk in `test/ai/program.test.js` checks that every string member
|
|
88
|
+
* declares a cap, and a check that has to interpret a regex to decide
|
|
89
|
+
* whether one is bounded is a weaker check than one that reads a
|
|
90
|
+
* number. */
|
|
91
|
+
const AS = {
|
|
92
|
+
type: 'string',
|
|
93
|
+
pattern: NAME_PATTERN,
|
|
94
|
+
maxLength: 32,
|
|
95
|
+
description: 'A short name for this step\'s result, used as "from" by a later step.',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One step's schema.
|
|
100
|
+
* @param {string} op
|
|
101
|
+
* @param {string} description
|
|
102
|
+
* @param {Record<string, any>} extra - members beyond `from`/`as`
|
|
103
|
+
* @param {string[]} [required] - beyond `from`, `as`
|
|
104
|
+
*/
|
|
105
|
+
function step(op, description, extra = {}, required = []) {
|
|
106
|
+
return {
|
|
107
|
+
title: op,
|
|
108
|
+
description,
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
op: { const: op },
|
|
112
|
+
from: FROM,
|
|
113
|
+
as: AS,
|
|
114
|
+
...extra,
|
|
115
|
+
},
|
|
116
|
+
required: ['op', 'from', 'as', ...required],
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The program schema.
|
|
123
|
+
*
|
|
124
|
+
* `queryRef` is the `$id` of an injected query grammar
|
|
125
|
+
* (`@jarenjs/json/schemas/jaren-query.schema.json`, or its LLM-profile
|
|
126
|
+
* twin — the twin is the better choice for constrained decoding, which
|
|
127
|
+
* is what it was derived for). Given one, `select` and `reduce` are
|
|
128
|
+
* shape-constrained as well as compile-gated, and the caller must pass
|
|
129
|
+
* the same grammar to `createStructuredOutput` as a `ref` so the
|
|
130
|
+
* validator can resolve it. Given none, `query` accepts any JSON value
|
|
131
|
+
* and the compile gate carries the whole weight.
|
|
132
|
+
*
|
|
133
|
+
* @param {{ queryRef?: string, maxSteps?: number }} [options]
|
|
134
|
+
* @returns {any} a JSON Schema document
|
|
135
|
+
*/
|
|
136
|
+
export function programSchema(options = {}) {
|
|
137
|
+
const query = options.queryRef === undefined
|
|
138
|
+
? { description: 'A jaren-query document.' }
|
|
139
|
+
: { $ref: options.queryRef, description: 'A jaren-query document.' };
|
|
140
|
+
const maxSteps = options.maxSteps ?? MAX_STEPS;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
$id: 'https://jarenjs.github.io/schemas/ai/program.json',
|
|
144
|
+
title: 'Environment program',
|
|
145
|
+
description: 'A plan over slots in the agent\'s environment. Steps name slots and never'
|
|
146
|
+
+ ' carry their content; the last step is always "answer".',
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: {
|
|
149
|
+
steps: {
|
|
150
|
+
type: 'array',
|
|
151
|
+
minItems: 1,
|
|
152
|
+
maxItems: maxSteps,
|
|
153
|
+
items: {
|
|
154
|
+
// anyOf, not oneOf: the branches are disjoint by their `op`
|
|
155
|
+
// const, and oneOf is the keyword provider implementations
|
|
156
|
+
// most often refuse (the same reason schemas/patch.js gives).
|
|
157
|
+
anyOf: [
|
|
158
|
+
step('chunk', 'Split a slot into addressable pieces.', {
|
|
159
|
+
strategy: { enum: ['size', 'line', 'separator'], description: 'How to cut. Default size.' },
|
|
160
|
+
size: { type: 'integer', minimum: 200, maximum: 100000, description: 'Piece size in characters.' },
|
|
161
|
+
}),
|
|
162
|
+
step('grep', 'Scan for a pattern and record which slots matched.', {
|
|
163
|
+
pattern: { type: 'string', minLength: 1, maxLength: PATTERN_MAX, description: 'A regular expression.' },
|
|
164
|
+
flags: { enum: ['i', 'm', 'im', ''], description: 'Regex flags. Default i.' },
|
|
165
|
+
limit: { type: 'integer', minimum: 1, maximum: 200, description: 'Maximum matches recorded.' },
|
|
166
|
+
}, ['pattern']),
|
|
167
|
+
step('select', 'Run a query over a JSON slot and store the result.', { query }, ['query']),
|
|
168
|
+
step('stat', 'Counts, sizes and shape of a slot or a family.'),
|
|
169
|
+
step('peek', 'Metadata and a head excerpt of one slot.'),
|
|
170
|
+
step('map', 'Ask the model once per piece. The ONLY step that calls a model.', {
|
|
171
|
+
prompt: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
minLength: 1,
|
|
174
|
+
maxLength: PROMPT_MAX,
|
|
175
|
+
description: 'What to ask about each piece. Ask for a JSON value; the piece is'
|
|
176
|
+
+ ' supplied automatically, so do not paste any content here.',
|
|
177
|
+
},
|
|
178
|
+
}, ['prompt']),
|
|
179
|
+
step('reduce', 'Combine a map\'s results with a query, into one slot.', {
|
|
180
|
+
query, outputSchema: { type: 'object', description: 'Declared JSON Schema for an inference-unknown result; validated before storage.' },
|
|
181
|
+
}, ['query']),
|
|
182
|
+
{
|
|
183
|
+
title: 'answer',
|
|
184
|
+
description: 'The last step: the slot the answer is read from.',
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
op: { const: 'answer' },
|
|
188
|
+
from: FROM,
|
|
189
|
+
chars: { type: 'integer', minimum: 1, maximum: 8000, description: 'How much of it to read.' },
|
|
190
|
+
},
|
|
191
|
+
required: ['op', 'from'],
|
|
192
|
+
additionalProperties: false,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
required: ['steps'],
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The program schema with the query seam empty — what a caller with no
|
|
204
|
+
* grammar injected authors against. */
|
|
205
|
+
export const PROGRAM_SCHEMA = programSchema();
|
package/src/toolbox.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} ToolDef
|
|
3
|
+
* @property {string} name
|
|
4
|
+
* @property {string} description
|
|
5
|
+
* @property {any} inputSchema - JSON Schema for the arguments object
|
|
6
|
+
* @property {(input: any) => any} execute - may return a value or a promise
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* @param {{ validator?: any }} [options] - a shared JarenValidator, if
|
|
10
|
+
* the host already has one
|
|
11
|
+
* @returns {{
|
|
12
|
+
* add: (def: ToolDef) => void,
|
|
13
|
+
* list: () => { name: string, description: string, inputSchema: any }[],
|
|
14
|
+
* toFunctionTools: () => any[],
|
|
15
|
+
* execute: (name: string, args: any) => any,
|
|
16
|
+
* }}
|
|
17
|
+
*/
|
|
18
|
+
export function createToolbox(options?: {
|
|
19
|
+
validator?: any;
|
|
20
|
+
}): {
|
|
21
|
+
add: (def: ToolDef) => void;
|
|
22
|
+
list: () => {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
inputSchema: any;
|
|
26
|
+
}[];
|
|
27
|
+
toFunctionTools: () => any[];
|
|
28
|
+
execute: (name: string, args: any) => any;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Publish validated tools through the shared browser adapter. Await `ready`
|
|
32
|
+
* for completion and call `dispose` when the host leaves. The optional context
|
|
33
|
+
* remains the second argument; undefined requests automatic discovery.
|
|
34
|
+
* @param {ReturnType<typeof createToolbox>} toolbox
|
|
35
|
+
* @param {unknown} [modelContext]
|
|
36
|
+
* @param {(error: unknown) => void} [onError]
|
|
37
|
+
* @param {Omit<import('@jarenjs/contract/webmcp').WebMcpOptions, 'context'|'onError'>} [options]
|
|
38
|
+
*/
|
|
39
|
+
export function registerModelContext(toolbox: ReturnType<typeof createToolbox>, modelContext?: unknown, onError?: (error: unknown) => void, options?: Omit<import("@jarenjs/contract/webmcp").WebMcpOptions, "context" | "onError">): {
|
|
40
|
+
ready: Promise<import("@jarenjs/contract/webmcp").WebMcpResult>;
|
|
41
|
+
dispose: () => Promise<import("@jarenjs/contract/webmcp").WebMcpResult>;
|
|
42
|
+
readonly status: "disposed" | "failed" | "pending" | "registered" | "unavailable";
|
|
43
|
+
};
|
|
44
|
+
export type ToolDef = {
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
/**
|
|
48
|
+
* - JSON Schema for the arguments object
|
|
49
|
+
*/
|
|
50
|
+
inputSchema: any;
|
|
51
|
+
/**
|
|
52
|
+
* - may return a value or a promise
|
|
53
|
+
*/
|
|
54
|
+
execute: (input: any) => any;
|
|
55
|
+
};
|
package/src/toolbox.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* The toolbox: a registry of tools an AI may call, each declared with
|
|
4
|
+
* a JSON Schema `inputSchema` that Jaren itself compiles and enforces
|
|
5
|
+
* before the tool runs — the suite guarding its own tools. One
|
|
6
|
+
* registry serves every surface that wants to drive the host app:
|
|
7
|
+
*
|
|
8
|
+
* - an embedded agent loop (`toFunctionTools()` produces the OpenAI
|
|
9
|
+
* function-calling definitions, `execute()` dispatches a call);
|
|
10
|
+
* - a browser-hosted agent over WebMCP (`registerModelContext()`
|
|
11
|
+
* publishes the same tools on either browser model-context root).
|
|
12
|
+
*
|
|
13
|
+
* `execute` never throws for content-level problems: an unknown tool
|
|
14
|
+
* or a throwing tool comes back as `{ error }`, invalid input as
|
|
15
|
+
* `{ error, errors, inputSchema }` plus a `hint` where the model sent
|
|
16
|
+
* JSON-encoded text for a structured property — results the calling
|
|
17
|
+
* model can read and recover from.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { JarenValidator } from '@jarenjs/validate';
|
|
21
|
+
import { registerWebMcp } from '@jarenjs/contract/webmcp';
|
|
22
|
+
|
|
23
|
+
import { checkOutcome } from '@jarenjs/core/check';
|
|
24
|
+
import { invalidInput } from '@tangleai/models/check';
|
|
25
|
+
|
|
26
|
+
/** Whether a property schema asks for structure (object or array). */
|
|
27
|
+
function wantsStructure(schema) {
|
|
28
|
+
const type = schema?.type;
|
|
29
|
+
return type === 'object' || type === 'array'
|
|
30
|
+
|| (Array.isArray(type) && (type.includes('object') || type.includes('array')));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The top-level properties that wanted structure but arrived as
|
|
35
|
+
* strings — after coercion failed, these are what went wrong.
|
|
36
|
+
* @param {any} inputSchema
|
|
37
|
+
* @param {any} input
|
|
38
|
+
* @returns {string[]}
|
|
39
|
+
*/
|
|
40
|
+
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);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A copy of `input` with string-valued top-level properties parsed as
|
|
50
|
+
* JSON wherever the schema wants an object or array — or `null` when
|
|
51
|
+
* nothing was coercible.
|
|
52
|
+
* @param {any} inputSchema
|
|
53
|
+
* @param {any} input
|
|
54
|
+
* @returns {any | null}
|
|
55
|
+
*/
|
|
56
|
+
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
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// not JSON text: keep the original so validation reports it
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return coerced;
|
|
74
|
+
}
|
|
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
|
+
/**
|
|
85
|
+
* @param {{ validator?: any }} [options] - a shared JarenValidator, if
|
|
86
|
+
* 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
|
+
*/
|
|
94
|
+
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
|
+
}
|
|
142
|
+
}
|
|
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
|
+
});
|
|
149
|
+
}
|
|
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;
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
return { error: /** @type {any} */ (err)?.message ?? String(err) };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { add, list, toFunctionTools, execute };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Publish validated tools through the shared browser adapter. Await `ready`
|
|
167
|
+
* for completion and call `dispose` when the host leaves. The optional context
|
|
168
|
+
* 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]
|
|
173
|
+
*/
|
|
174
|
+
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 });
|
|
178
|
+
}
|