@cubicecho/agent-core 2.23.0 → 2.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/README.md +14 -9
- package/dist/agent-loop.d.ts +84 -204
- package/dist/agent-loop.js +416 -643
- package/dist/calibration.d.ts +14 -10
- package/dist/calibration.js +30 -22
- package/dist/capabilities.d.ts +140 -35
- package/dist/capabilities.js +345 -152
- package/dist/client.d.ts +104 -35
- package/dist/client.js +210 -78
- package/dist/compaction.d.ts +103 -60
- package/dist/compaction.js +163 -93
- package/dist/config.d.ts +48 -12
- package/dist/config.js +20 -1
- package/dist/continuation.d.ts +25 -17
- package/dist/continuation.js +70 -47
- package/dist/digest.d.ts +7 -0
- package/dist/digest.js +18 -0
- package/dist/errors.d.ts +24 -15
- package/dist/errors.js +23 -15
- package/dist/events.d.ts +108 -58
- package/dist/events.js +265 -128
- package/dist/guards.d.ts +53 -0
- package/dist/guards.js +55 -0
- package/dist/hook-events.d.ts +38 -0
- package/dist/hook-events.js +36 -0
- package/dist/hooks.d.ts +105 -83
- package/dist/hooks.js +224 -139
- package/dist/index.d.ts +30 -25
- package/dist/index.js +29 -23
- package/dist/ledger.d.ts +62 -30
- package/dist/ledger.js +163 -56
- package/dist/platform.d.ts +13 -0
- package/dist/platform.js +13 -0
- package/dist/preselect.d.ts +222 -0
- package/dist/preselect.js +290 -0
- package/dist/request-body.d.ts +32 -0
- package/dist/request-body.js +47 -0
- package/dist/reset.d.ts +1 -0
- package/dist/reset.js +7 -6
- package/dist/retry.d.ts +40 -143
- package/dist/retry.js +79 -285
- package/dist/run-calls.d.ts +123 -0
- package/dist/run-calls.js +351 -0
- package/dist/run-turn.d.ts +44 -16
- package/dist/run-turn.js +74 -37
- package/dist/runtime.d.ts +23 -17
- package/dist/runtime.js +34 -24
- package/dist/schema-compat.d.ts +51 -11
- package/dist/schema-compat.js +375 -193
- package/dist/scope.d.ts +28 -5
- package/dist/scope.js +37 -10
- package/dist/side-task.d.ts +127 -39
- package/dist/side-task.js +189 -100
- package/dist/snapshot.d.ts +12 -7
- package/dist/snapshot.js +87 -65
- package/dist/spec.d.ts +66 -36
- package/dist/spec.js +514 -265
- package/dist/stream.d.ts +73 -16
- package/dist/stream.js +228 -98
- package/dist/thinking.d.ts +23 -8
- package/dist/thinking.js +96 -46
- package/dist/tokens.d.ts +171 -6
- package/dist/tokens.js +326 -6
- package/dist/tool-calls.d.ts +89 -15
- package/dist/tool-calls.js +205 -85
- package/dist/tool-loading.d.ts +109 -187
- package/dist/tool-loading.js +286 -372
- package/dist/wire.d.ts +81 -0
- package/dist/wire.js +73 -0
- package/llms.txt +87 -43
- package/package.json +4 -4
package/dist/schema-compat.js
CHANGED
|
@@ -1,138 +1,222 @@
|
|
|
1
|
-
|
|
1
|
+
import { getOrCreate, isRecord } from './guards.js';
|
|
2
|
+
import { FUNCTION_TOOL, SchemaType } from './wire.js';
|
|
2
3
|
/** Lookahead and lookbehind: `(?=`, `(?!`, `(?<=`, `(?<!`. */
|
|
3
4
|
const LOOKAROUND = /\(\?<?[=!]/;
|
|
4
|
-
const PRIMITIVES = new Set(
|
|
5
|
-
|
|
5
|
+
const PRIMITIVES = new Set(Object.values(SchemaType));
|
|
6
|
+
/**
|
|
7
|
+
* An object schema with no properties: what a position that holds no usable schema becomes.
|
|
8
|
+
*
|
|
9
|
+
* @returns A new object on every call, so no two callers share one.
|
|
10
|
+
*/
|
|
11
|
+
const EMPTY_OBJECT = () => ({ type: SchemaType.Object, properties: {} });
|
|
6
12
|
/** Keys whose value is a schema, or a list of them — several are spelled both ways. */
|
|
7
13
|
const SCHEMA_KEYS = new Set([
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
'items',
|
|
15
|
+
'additionalProperties',
|
|
16
|
+
'not',
|
|
17
|
+
'if',
|
|
18
|
+
'then',
|
|
19
|
+
'else',
|
|
20
|
+
'contains',
|
|
21
|
+
'propertyNames',
|
|
22
|
+
'anyOf',
|
|
23
|
+
'oneOf',
|
|
24
|
+
'allOf',
|
|
25
|
+
'prefixItems',
|
|
20
26
|
]);
|
|
21
27
|
/** Keys whose value is a name -> schema map. */
|
|
22
|
-
const SCHEMA_MAPS = new Set([
|
|
28
|
+
const SCHEMA_MAPS = new Set(['properties', 'patternProperties', '$defs', 'definitions']);
|
|
29
|
+
/** The two spellings of a schema's own pool of named definitions. */
|
|
30
|
+
const POOL_KEYS = ['definitions', '$defs'];
|
|
31
|
+
/**
|
|
32
|
+
* One keyword's value with `fn` applied wherever it holds a schema, and untouched where it holds
|
|
33
|
+
* data — which is what keeps a walk out of `default`, `enum` and `const`.
|
|
34
|
+
*
|
|
35
|
+
* @param key - The keyword the value sits under, which is what says whether it holds schemas.
|
|
36
|
+
* @param value - What the keyword holds. Not mutated.
|
|
37
|
+
* @param fn - The rewrite for one schema position. Handed whatever is there, schema or not.
|
|
38
|
+
* @returns A new array or map of what `fn` gave back, or `fn`'s answer for a single schema. For
|
|
39
|
+
* any other keyword, and for a map keyword that holds no map, `value` itself.
|
|
40
|
+
*/
|
|
41
|
+
const mapChildren = (key, value, fn) => {
|
|
42
|
+
if (SCHEMA_KEYS.has(key)) {
|
|
43
|
+
return Array.isArray(value) ? value.map((sub) => fn(sub)) : fn(value);
|
|
44
|
+
}
|
|
45
|
+
// The keys here are argument names; only the values are schemas.
|
|
46
|
+
if (SCHEMA_MAPS.has(key) && isRecord(value)) {
|
|
47
|
+
return Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, fn(sub)]));
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
};
|
|
23
51
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
52
|
+
* The names a schema's `required` lists, without whatever in it is not a name.
|
|
53
|
+
*
|
|
54
|
+
* @param node - The schema whose `required` is read.
|
|
55
|
+
* @returns A new array, empty where `required` is missing or is not a list.
|
|
56
|
+
*/
|
|
57
|
+
const requiredOf = (node) => Array.isArray(node.required) ? node.required.filter((name) => typeof name === 'string') : [];
|
|
58
|
+
/**
|
|
59
|
+
* Coerces one schema position.
|
|
60
|
+
*
|
|
61
|
+
* @param node - Whatever sits where a schema belongs. Not mutated.
|
|
62
|
+
* @returns A boolean schema as it is, and an object normalised into a new one. A bare primitive
|
|
63
|
+
* type name becomes `{ type }`; `"object"`, any other string and anything else at all become an
|
|
64
|
+
* object with no properties.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* Malformed MCP output sometimes puts a bare type name where a whole schema belongs, which the
|
|
68
|
+
* grammar converter reports as `Unrecognized schema: "object"`.
|
|
26
69
|
*/
|
|
27
70
|
function asSchema(node) {
|
|
28
|
-
if (typeof node ===
|
|
29
|
-
return PRIMITIVES.has(node) && node !==
|
|
30
|
-
|
|
71
|
+
if (typeof node === 'string') {
|
|
72
|
+
return PRIMITIVES.has(node) && node !== SchemaType.Object ? { type: node } : EMPTY_OBJECT();
|
|
73
|
+
}
|
|
74
|
+
if (typeof node === 'boolean') {
|
|
31
75
|
return node;
|
|
32
|
-
|
|
76
|
+
}
|
|
77
|
+
if (!isRecord(node)) {
|
|
33
78
|
return EMPTY_OBJECT();
|
|
79
|
+
}
|
|
34
80
|
return normalize(node);
|
|
35
81
|
}
|
|
36
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Recursively rewrites the shapes llama.cpp's grammar converter cannot represent.
|
|
84
|
+
*
|
|
85
|
+
* @param node - The schema as written. Not mutated.
|
|
86
|
+
* @returns A new schema, rewritten at every schema position under it. Where it holds a `$ref`,
|
|
87
|
+
* the reference alone.
|
|
88
|
+
*/
|
|
37
89
|
function normalize(node) {
|
|
38
|
-
const
|
|
90
|
+
const built = {};
|
|
39
91
|
for (const [key, value] of Object.entries(node)) {
|
|
40
92
|
// `type: ["string", "null"]` — the converter only accepts a single string type.
|
|
41
|
-
if (key ===
|
|
42
|
-
const names = value.filter((item) => typeof item ===
|
|
43
|
-
const concrete = names.filter((name) => name !==
|
|
44
|
-
if (names.includes(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
else if (SCHEMA_MAPS.has(key) && isObject(value)) {
|
|
57
|
-
out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, asSchema(sub)]));
|
|
93
|
+
if (key === 'type' && Array.isArray(value)) {
|
|
94
|
+
const names = value.filter((item) => typeof item === 'string');
|
|
95
|
+
const concrete = names.filter((name) => name !== SchemaType.Null);
|
|
96
|
+
if (names.includes(SchemaType.Null)) {
|
|
97
|
+
built.nullable = true;
|
|
98
|
+
}
|
|
99
|
+
if (concrete.length === 1) {
|
|
100
|
+
built.type = concrete[0];
|
|
101
|
+
}
|
|
102
|
+
else if (concrete.length > 1) {
|
|
103
|
+
built.anyOf = concrete.map((name) => ({ type: name }));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
built.type = SchemaType.Null;
|
|
107
|
+
}
|
|
58
108
|
}
|
|
59
109
|
else {
|
|
60
|
-
|
|
110
|
+
built[key] = mapChildren(key, value, asSchema);
|
|
61
111
|
}
|
|
62
112
|
}
|
|
63
|
-
collapseNullableUnion(
|
|
113
|
+
const out = collapseNullableUnion(built);
|
|
64
114
|
// A grammar is context-free; lookaround is not expressible in one at all, so no converter
|
|
65
115
|
// can accept it. Dropping it costs one advisory constraint on one string field.
|
|
66
|
-
if (typeof out.pattern ===
|
|
116
|
+
if (typeof out.pattern === 'string' && LOOKAROUND.test(out.pattern)) {
|
|
67
117
|
delete out.pattern;
|
|
118
|
+
}
|
|
68
119
|
// `{"type": "object"}` with no properties produces invalid GBNF.
|
|
69
|
-
if (out.type ===
|
|
120
|
+
if (out.type === SchemaType.Object && !isRecord(out.properties)) {
|
|
70
121
|
out.properties = {};
|
|
122
|
+
}
|
|
71
123
|
// Strict validators reject any sibling of `$ref`, and draft-07 ignores them, so a reference
|
|
72
124
|
// stands alone or not at all. This is not hypothetical tidying: collapsing `anyOf: [{$ref},
|
|
73
125
|
// {type: "null"}]` — the shape a schema-generated server emits at every optional argument —
|
|
74
126
|
// lands `nullable` right next to the `$ref` that survived. Whatever is dropped here was
|
|
75
127
|
// already unreadable to a conforming consumer; optionality still lives in the parent's
|
|
76
128
|
// `required`.
|
|
77
|
-
if (
|
|
129
|
+
if ('$ref' in out) {
|
|
78
130
|
return { $ref: out.$ref };
|
|
131
|
+
}
|
|
79
132
|
return out;
|
|
80
133
|
}
|
|
81
134
|
/**
|
|
82
135
|
* `{anyOf: [{type: "string"}, {type: "null"}]}` is how Pydantic-backed MCP servers spell an
|
|
83
|
-
* optional field.
|
|
84
|
-
*
|
|
136
|
+
* optional field.
|
|
137
|
+
*
|
|
138
|
+
* @param node - The schema whose `anyOf` and `oneOf` are looked at. Not mutated.
|
|
139
|
+
* @returns `node` itself unless a union is one real branch beside `null` ones. Then a copy without
|
|
140
|
+
* that union, marked `nullable`, with the real branch's keywords laid over its own.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* Optionality already lives in the parent's `required`, so keep the one real branch. A union with
|
|
144
|
+
* two real branches is meaningful and is left alone.
|
|
85
145
|
*/
|
|
86
146
|
function collapseNullableUnion(node) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
147
|
+
let out = node;
|
|
148
|
+
for (const key of ['anyOf', 'oneOf']) {
|
|
149
|
+
const variants = out[key];
|
|
150
|
+
if (!Array.isArray(variants)) {
|
|
90
151
|
continue;
|
|
91
|
-
|
|
92
|
-
|
|
152
|
+
}
|
|
153
|
+
const concrete = variants.filter((item) => !(isRecord(item) && item.type === SchemaType.Null));
|
|
154
|
+
if (concrete.length !== 1 || concrete.length === variants.length) {
|
|
93
155
|
continue;
|
|
94
|
-
|
|
95
|
-
|
|
156
|
+
}
|
|
157
|
+
const { [key]: _collapsed, ...rest } = out;
|
|
158
|
+
out = { ...rest, nullable: true, ...(isRecord(concrete[0]) ? concrete[0] : {}) };
|
|
96
159
|
}
|
|
160
|
+
return out;
|
|
97
161
|
}
|
|
98
162
|
/** Combinators at the top level of a parameters schema; strict backends reject them outright. */
|
|
99
|
-
const TOP_LEVEL_COMBINATORS = [
|
|
163
|
+
const TOP_LEVEL_COMBINATORS = ['allOf', 'anyOf', 'oneOf', 'enum', 'not'];
|
|
100
164
|
/** `#/definitions/Args` or `#/$defs/Args` — a pointer into this schema's own definitions. */
|
|
101
165
|
const LOCAL_POINTER = /^#\/(definitions|\$defs)\/([^/]+)$/;
|
|
102
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* The `definitions` and `$defs` that a local pointer in this schema resolves against.
|
|
168
|
+
*
|
|
169
|
+
* @param parameters - The root schema, which is where the pools live.
|
|
170
|
+
* @returns A new object holding whichever of the two the root has as an object — the pools
|
|
171
|
+
* themselves, not copies. Empty where it has neither.
|
|
172
|
+
*/
|
|
103
173
|
function poolsOf(parameters) {
|
|
104
174
|
const defs = {};
|
|
105
|
-
for (const key of
|
|
106
|
-
if (
|
|
175
|
+
for (const key of POOL_KEYS) {
|
|
176
|
+
if (isRecord(parameters[key])) {
|
|
107
177
|
defs[key] = parameters[key];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
108
180
|
return defs;
|
|
109
181
|
}
|
|
110
182
|
/**
|
|
111
183
|
* Follows a chain of local references to the schema it arrives at, or `undefined` where it
|
|
112
184
|
* arrives at none — a pointer into another document, one that comes back around to itself, or a
|
|
113
|
-
* name the pools do not hold.
|
|
114
|
-
*
|
|
185
|
+
* name the pools do not hold.
|
|
186
|
+
*
|
|
187
|
+
* @param node - The schema position to resolve, reference or not.
|
|
188
|
+
* @param defs - The pools to resolve against, as `poolsOf` collects them from the root.
|
|
189
|
+
* @returns The schema arrived at — the object itself, not a copy — or `undefined`, which is also
|
|
190
|
+
* the answer where the chain ends on something that is not an object.
|
|
115
191
|
*
|
|
116
|
-
* @
|
|
117
|
-
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* An object that is not a reference resolves to itself, so a caller can hand this a branch without
|
|
194
|
+
* first asking which spelling it is.
|
|
118
195
|
*/
|
|
119
196
|
function resolveRef(node, defs) {
|
|
120
197
|
const seen = new Set();
|
|
121
198
|
let current = node;
|
|
122
|
-
while (
|
|
199
|
+
while (isRecord(current) && typeof current.$ref === 'string') {
|
|
123
200
|
const pointer = current.$ref;
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
201
|
+
const [, poolKey, name] = LOCAL_POINTER.exec(pointer) ?? [];
|
|
202
|
+
if (!name || seen.has(pointer)) {
|
|
126
203
|
return undefined;
|
|
204
|
+
}
|
|
127
205
|
seen.add(pointer);
|
|
128
|
-
const pool = defs[
|
|
129
|
-
current =
|
|
206
|
+
const pool = defs[poolKey];
|
|
207
|
+
current = isRecord(pool) ? pool[name] : undefined;
|
|
130
208
|
}
|
|
131
|
-
return
|
|
209
|
+
return isRecord(current) ? current : undefined;
|
|
132
210
|
}
|
|
133
211
|
/**
|
|
134
212
|
* Replaces a root-level `$ref` with what it points at.
|
|
135
213
|
*
|
|
214
|
+
* @param parameters - The root schema. Not mutated.
|
|
215
|
+
* @returns `parameters` itself where its `$ref` is not a string. Otherwise a new object: what the
|
|
216
|
+
* pointer lands on with the root's pools laid over it, or an object with no properties where it
|
|
217
|
+
* lands nowhere.
|
|
218
|
+
*
|
|
219
|
+
* @remarks
|
|
136
220
|
* Dropping the siblings of a `$ref` is right at a nested position and wrong at this one: the
|
|
137
221
|
* siblings here are the `definitions` the pointer needs, so the reference is left dangling and
|
|
138
222
|
* `properties` is then backfilled empty below. The tool goes out advertising no arguments at
|
|
@@ -140,8 +224,9 @@ function resolveRef(node, defs) {
|
|
|
140
224
|
* generator emits this shape whenever the argument object is a named type.
|
|
141
225
|
*/
|
|
142
226
|
function inlineRootRef(parameters) {
|
|
143
|
-
if (typeof parameters.$ref !==
|
|
227
|
+
if (typeof parameters.$ref !== 'string') {
|
|
144
228
|
return parameters;
|
|
229
|
+
}
|
|
145
230
|
const defs = poolsOf(parameters);
|
|
146
231
|
const resolved = resolveRef(parameters, defs);
|
|
147
232
|
// A pointer that lands nowhere has nothing here to resolve against. An object with no
|
|
@@ -152,6 +237,12 @@ function inlineRootRef(parameters) {
|
|
|
152
237
|
/**
|
|
153
238
|
* Folds a root `allOf` into the root itself.
|
|
154
239
|
*
|
|
240
|
+
* @param schema - The root schema. Not mutated.
|
|
241
|
+
* @returns `schema` itself where `allOf` is not a list, or where neither it nor the root has a
|
|
242
|
+
* property. Otherwise a copy holding the root's properties with each branch's laid over them and
|
|
243
|
+
* every `required` name unioned — `allOf` still on it, for `sanitizeSchema` to delete.
|
|
244
|
+
*
|
|
245
|
+
* @remarks
|
|
155
246
|
* It is the other way a generated schema spells "the arguments are this named type", and
|
|
156
247
|
* deleting it outright below threw the arguments away while leaving the `required` that named
|
|
157
248
|
* them. A branch is far more often a reference than an inline object — a named type is exactly
|
|
@@ -159,33 +250,40 @@ function inlineRootRef(parameters) {
|
|
|
159
250
|
* One that resolves nowhere is not something to guess at, and falls through to `pruneRequired`,
|
|
160
251
|
* which at least keeps the result self-consistent.
|
|
161
252
|
*/
|
|
162
|
-
function mergeRootAllOf(
|
|
163
|
-
const branches =
|
|
164
|
-
if (!Array.isArray(branches))
|
|
165
|
-
return;
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
const
|
|
253
|
+
function mergeRootAllOf(schema) {
|
|
254
|
+
const branches = schema.allOf;
|
|
255
|
+
if (!Array.isArray(branches)) {
|
|
256
|
+
return schema;
|
|
257
|
+
}
|
|
258
|
+
const defs = poolsOf(schema);
|
|
259
|
+
const properties = isRecord(schema.properties) ? { ...schema.properties } : {};
|
|
260
|
+
const required = new Set(requiredOf(schema));
|
|
169
261
|
for (const raw of branches) {
|
|
170
262
|
const branch = resolveRef(raw, defs);
|
|
171
|
-
if (!branch)
|
|
263
|
+
if (!branch) {
|
|
172
264
|
continue;
|
|
173
|
-
|
|
265
|
+
}
|
|
266
|
+
if (isRecord(branch.properties)) {
|
|
174
267
|
Object.assign(properties, branch.properties);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
268
|
+
}
|
|
269
|
+
for (const name of requiredOf(branch)) {
|
|
270
|
+
required.add(name);
|
|
271
|
+
}
|
|
179
272
|
}
|
|
180
|
-
if (!Object.keys(properties).length)
|
|
181
|
-
return;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
out.required = [...required];
|
|
273
|
+
if (!Object.keys(properties).length) {
|
|
274
|
+
return schema;
|
|
275
|
+
}
|
|
276
|
+
return { ...schema, properties, ...(required.size ? { required: [...required] } : {}) };
|
|
185
277
|
}
|
|
186
278
|
/**
|
|
187
279
|
* Folds a root `anyOf` or `oneOf` into the root itself.
|
|
188
280
|
*
|
|
281
|
+
* @param schema - The root schema. Not mutated.
|
|
282
|
+
* @returns `schema` itself where no union's branches hold a property. Otherwise a copy holding the
|
|
283
|
+
* root's properties with the branches' laid over them, and the root's `required` plus the names
|
|
284
|
+
* every branch asks for — the union still on it, for `sanitizeSchema` to delete.
|
|
285
|
+
*
|
|
286
|
+
* @remarks
|
|
189
287
|
* The third spelling of "the arguments are this named type", after `$ref` and `allOf`, and the
|
|
190
288
|
* one still going out empty: a union of real object shapes has no `null` branch for
|
|
191
289
|
* `collapseNullableUnion` to take apart, so it reached the delete below intact and every
|
|
@@ -197,12 +295,14 @@ function mergeRootAllOf(out) {
|
|
|
197
295
|
* this schema's own pools first. One that resolves nowhere still cannot vouch for a name, so its
|
|
198
296
|
* presence alone empties `required`.
|
|
199
297
|
*/
|
|
200
|
-
function mergeRootUnion(
|
|
201
|
-
const defs = poolsOf(
|
|
202
|
-
|
|
298
|
+
function mergeRootUnion(schema) {
|
|
299
|
+
const defs = poolsOf(schema);
|
|
300
|
+
let out = schema;
|
|
301
|
+
for (const key of ['anyOf', 'oneOf']) {
|
|
203
302
|
const branches = out[key];
|
|
204
|
-
if (!Array.isArray(branches))
|
|
303
|
+
if (!Array.isArray(branches)) {
|
|
205
304
|
continue;
|
|
305
|
+
}
|
|
206
306
|
const properties = {};
|
|
207
307
|
// `null` until a branch has been read, which is what tells "no branches yet" apart from
|
|
208
308
|
// "the branches agreed on nothing".
|
|
@@ -214,27 +314,30 @@ function mergeRootUnion(out) {
|
|
|
214
314
|
shared = new Set();
|
|
215
315
|
continue;
|
|
216
316
|
}
|
|
217
|
-
if (
|
|
317
|
+
if (isRecord(branch.properties)) {
|
|
218
318
|
Object.assign(properties, branch.properties);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
: [];
|
|
319
|
+
}
|
|
320
|
+
const names = requiredOf(branch);
|
|
222
321
|
shared = previous === null ? new Set(names) : new Set(names.filter((n) => previous.has(n)));
|
|
223
322
|
}
|
|
224
|
-
if (!Object.keys(properties).length)
|
|
323
|
+
if (!Object.keys(properties).length) {
|
|
225
324
|
continue;
|
|
226
|
-
out.properties = { ...(isObject(out.properties) ? out.properties : {}), ...properties };
|
|
227
|
-
if (shared?.size) {
|
|
228
|
-
const already = Array.isArray(out.required)
|
|
229
|
-
? out.required.filter((name) => typeof name === "string")
|
|
230
|
-
: [];
|
|
231
|
-
out.required = [...new Set([...already, ...shared])];
|
|
232
325
|
}
|
|
326
|
+
out = {
|
|
327
|
+
...out,
|
|
328
|
+
properties: { ...(isRecord(out.properties) ? out.properties : {}), ...properties },
|
|
329
|
+
...(shared?.size ? { required: [...new Set([...requiredOf(out), ...shared])] } : {}),
|
|
330
|
+
};
|
|
233
331
|
}
|
|
332
|
+
return out;
|
|
234
333
|
}
|
|
235
334
|
/**
|
|
236
335
|
* Every `$ref` string anywhere under a node, walked as arbitrary JSON rather than as a schema.
|
|
237
336
|
*
|
|
337
|
+
* @param node - Anything. A value that is neither an array nor an object holds no pointer.
|
|
338
|
+
* @param into - Where the pointers go. Written to, which is how the walk hands them back.
|
|
339
|
+
*
|
|
340
|
+
* @remarks
|
|
238
341
|
* The keyword-aware walk `strip` does is the wrong way round for this one. Missing a pointer
|
|
239
342
|
* here means deleting a definition that something still refers to, which breaks the schema;
|
|
240
343
|
* finding one that was really a string sitting in a `default` or an `enum` costs a definition
|
|
@@ -242,22 +345,31 @@ function mergeRootUnion(out) {
|
|
|
242
345
|
*/
|
|
243
346
|
function collectRefs(node, into) {
|
|
244
347
|
if (Array.isArray(node)) {
|
|
245
|
-
for (const item of node)
|
|
348
|
+
for (const item of node) {
|
|
246
349
|
collectRefs(item, into);
|
|
350
|
+
}
|
|
247
351
|
return;
|
|
248
352
|
}
|
|
249
|
-
if (!
|
|
353
|
+
if (!isRecord(node)) {
|
|
250
354
|
return;
|
|
355
|
+
}
|
|
251
356
|
for (const [key, value] of Object.entries(node)) {
|
|
252
|
-
if (key ===
|
|
357
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
253
358
|
into.add(value);
|
|
254
|
-
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
255
361
|
collectRefs(value, into);
|
|
362
|
+
}
|
|
256
363
|
}
|
|
257
364
|
}
|
|
258
365
|
/**
|
|
259
366
|
* Drops the `definitions` and `$defs` entries that nothing points at any more.
|
|
260
367
|
*
|
|
368
|
+
* @param schema - The root schema, pools and all. Not mutated.
|
|
369
|
+
* @returns `schema` itself where it has no pool, or every definition is still reached. Otherwise a
|
|
370
|
+
* copy whose pools hold only what is reached, and that lacks a pool nothing reaches into.
|
|
371
|
+
*
|
|
372
|
+
* @remarks
|
|
261
373
|
* The rewrites above delete whole subtrees — a root combinator once its branches are folded in,
|
|
262
374
|
* every sibling of a `$ref`, the branch of a union that was only ever `null` — and the pointers
|
|
263
375
|
* go with them while the pools they named stay behind. On a real Gmail or filesystem schema
|
|
@@ -268,80 +380,122 @@ function collectRefs(node, into) {
|
|
|
268
380
|
* name another; a cycle among them terminates on the `has` check, whether or not anything
|
|
269
381
|
* outside it still refers in.
|
|
270
382
|
*/
|
|
271
|
-
function pruneDefs(
|
|
272
|
-
const pools =
|
|
273
|
-
if (!pools.length)
|
|
274
|
-
return;
|
|
383
|
+
function pruneDefs(schema) {
|
|
384
|
+
const pools = POOL_KEYS.filter((key) => isRecord(schema[key]));
|
|
385
|
+
if (!pools.length) {
|
|
386
|
+
return schema;
|
|
387
|
+
}
|
|
275
388
|
const live = {};
|
|
276
389
|
const visit = (node) => {
|
|
277
390
|
const pointers = new Set();
|
|
278
391
|
collectRefs(node, pointers);
|
|
279
392
|
for (const pointer of pointers) {
|
|
280
393
|
const target = LOCAL_POINTER.exec(pointer);
|
|
281
|
-
if (!target)
|
|
394
|
+
if (!target) {
|
|
282
395
|
continue;
|
|
396
|
+
}
|
|
283
397
|
const [, poolKey, name] = target;
|
|
284
|
-
const pool =
|
|
285
|
-
if (!
|
|
398
|
+
const pool = schema[poolKey];
|
|
399
|
+
if (!isRecord(pool) || !(name in pool)) {
|
|
286
400
|
continue;
|
|
401
|
+
}
|
|
287
402
|
live[poolKey] ??= new Set();
|
|
288
403
|
const names = live[poolKey];
|
|
289
|
-
if (names.has(name))
|
|
404
|
+
if (names.has(name)) {
|
|
290
405
|
continue;
|
|
406
|
+
}
|
|
291
407
|
names.add(name);
|
|
292
408
|
visit(pool[name]);
|
|
293
409
|
}
|
|
294
410
|
};
|
|
295
411
|
// The pools themselves are not roots: a definition is reached from the schema body, or by
|
|
296
412
|
// another definition that was, or not at all.
|
|
297
|
-
const body = { ...
|
|
298
|
-
for (const key of pools)
|
|
413
|
+
const body = { ...schema };
|
|
414
|
+
for (const key of pools) {
|
|
299
415
|
delete body[key];
|
|
416
|
+
}
|
|
300
417
|
visit(body);
|
|
418
|
+
let pruned = schema;
|
|
301
419
|
for (const key of pools) {
|
|
302
420
|
const names = live[key];
|
|
303
421
|
if (!names?.size) {
|
|
304
|
-
|
|
422
|
+
const { [key]: _unreached, ...rest } = pruned;
|
|
423
|
+
pruned = rest;
|
|
305
424
|
continue;
|
|
306
425
|
}
|
|
307
|
-
const pool =
|
|
308
|
-
if (names.size === Object.keys(pool).length)
|
|
426
|
+
const pool = schema[key];
|
|
427
|
+
if (names.size === Object.keys(pool).length) {
|
|
309
428
|
continue;
|
|
310
|
-
|
|
429
|
+
}
|
|
430
|
+
const reached = Object.entries(pool).filter(([name]) => names.has(name));
|
|
431
|
+
pruned = { ...pruned, [key]: Object.fromEntries(reached) };
|
|
311
432
|
}
|
|
433
|
+
return pruned;
|
|
312
434
|
}
|
|
313
435
|
/**
|
|
314
436
|
* A required argument that is not in `properties` is one no caller can supply and no strict
|
|
315
|
-
* validator will accept.
|
|
437
|
+
* validator will accept.
|
|
438
|
+
*
|
|
439
|
+
* @param schema - The schema whose `required` is checked against its own `properties`. Not mutated.
|
|
440
|
+
* @returns `schema` itself where `required` is not a list. Otherwise a copy whose `required` names
|
|
441
|
+
* only what `properties` holds, or that has no `required` where no name is left.
|
|
442
|
+
*
|
|
443
|
+
* @remarks
|
|
444
|
+
* Anything the rewrites above removed, `required` may still name.
|
|
316
445
|
*/
|
|
317
|
-
function pruneRequired(
|
|
318
|
-
if (!Array.isArray(
|
|
319
|
-
return;
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
446
|
+
function pruneRequired(schema) {
|
|
447
|
+
if (!Array.isArray(schema.required)) {
|
|
448
|
+
return schema;
|
|
449
|
+
}
|
|
450
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
451
|
+
const kept = schema.required.filter((name) => typeof name === 'string' && name in properties);
|
|
452
|
+
if (kept.length) {
|
|
453
|
+
return { ...schema, required: kept };
|
|
454
|
+
}
|
|
455
|
+
const { required: _unmet, ...rest } = schema;
|
|
456
|
+
return rest;
|
|
326
457
|
}
|
|
327
|
-
|
|
328
|
-
|
|
458
|
+
/**
|
|
459
|
+
* One JSON Schema as a strict server will accept it, for a schema that is not a tool's parameters.
|
|
460
|
+
*
|
|
461
|
+
* @param schema - The schema as written. Never mutated. Its root is held to an object, as a tool's
|
|
462
|
+
* parameters are, and anything that is not a schema at all comes back as an object with no
|
|
463
|
+
* properties.
|
|
464
|
+
* @returns A new object: `type: "object"` with a `properties` map, no combinator at the root, no
|
|
465
|
+
* `required` name without a property, and no definition that nothing points at.
|
|
466
|
+
*
|
|
467
|
+
* @remarks
|
|
468
|
+
* What `sanitizeTools` does to each tool, without the tool: a `response_format` schema goes through
|
|
469
|
+
* the same grammar converter a tool's parameters do, and a caller holding only the schema had to
|
|
470
|
+
* wrap it in a definition to get here. Nothing is remembered — the cache is keyed on a tool
|
|
471
|
+
* definition's identity, and a bare schema has no object that stands for it across requests.
|
|
472
|
+
*/
|
|
473
|
+
export function sanitizeSchema(schema) {
|
|
474
|
+
if (!isRecord(schema)) {
|
|
329
475
|
return EMPTY_OBJECT();
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
mergeRootUnion(
|
|
333
|
-
for (const key of TOP_LEVEL_COMBINATORS)
|
|
476
|
+
}
|
|
477
|
+
// A copy of its own, so what is deleted and set below is this function's and nobody else's.
|
|
478
|
+
const out = { ...mergeRootUnion(mergeRootAllOf(normalize(inlineRootRef(schema)))) };
|
|
479
|
+
for (const key of TOP_LEVEL_COMBINATORS) {
|
|
334
480
|
delete out[key];
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
481
|
+
}
|
|
482
|
+
if (out.type !== SchemaType.Object) {
|
|
483
|
+
out.type = SchemaType.Object;
|
|
484
|
+
}
|
|
485
|
+
if (!isRecord(out.properties)) {
|
|
338
486
|
out.properties = {};
|
|
339
|
-
|
|
340
|
-
pruneDefs(out);
|
|
341
|
-
return out;
|
|
487
|
+
}
|
|
488
|
+
return pruneDefs(pruneRequired(out));
|
|
342
489
|
}
|
|
343
|
-
/**
|
|
344
|
-
|
|
490
|
+
/**
|
|
491
|
+
* Rewrites one tool's parameters, leaving a non-function tool alone.
|
|
492
|
+
*
|
|
493
|
+
* @param tool - The definition. Not mutated.
|
|
494
|
+
* @param fn - The rewrite. Handed the parameters as they are, a missing one included.
|
|
495
|
+
* @returns A new definition with what `fn` gave back as its parameters, or `tool` itself where it
|
|
496
|
+
* is not a function tool.
|
|
497
|
+
*/
|
|
498
|
+
const mapTool = (tool, fn) => tool.type === FUNCTION_TOOL
|
|
345
499
|
? {
|
|
346
500
|
...tool,
|
|
347
501
|
function: { ...tool.function, parameters: fn(tool.function.parameters) },
|
|
@@ -350,6 +504,7 @@ const mapTool = (tool, fn) => tool.type === "function"
|
|
|
350
504
|
/**
|
|
351
505
|
* Both rewrites are cached against the tool object rather than recomputed.
|
|
352
506
|
*
|
|
507
|
+
* @remarks
|
|
353
508
|
* The agent loop rebuilds its tool array on every iteration of every step, and normalising a
|
|
354
509
|
* couple of dozen MCP schemas is the only walk in a run that is neither a request nor a query.
|
|
355
510
|
* The pool hands out the same definition objects for the life of a connection, so identity is
|
|
@@ -368,88 +523,115 @@ const mapTool = (tool, fn) => tool.type === "function"
|
|
|
368
523
|
*/
|
|
369
524
|
const sanitized = new WeakMap();
|
|
370
525
|
const relaxed = new WeakMap();
|
|
371
|
-
/**
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
526
|
+
/**
|
|
527
|
+
* Looks one up, computing and remembering it on a miss.
|
|
528
|
+
*
|
|
529
|
+
* @param cache - The memory of one rewrite, keyed on a definition's identity. Gains an entry for
|
|
530
|
+
* every tool it did not hold.
|
|
531
|
+
* @param tools - The definitions to look up. Not mutated.
|
|
532
|
+
* @param fn - The rewrite the cache remembers, run on a tool's parameters only on a miss.
|
|
533
|
+
* @returns A new array in the order of `tools`, whose entries are the remembered definitions — the
|
|
534
|
+
* same object each time the same tool is asked for.
|
|
535
|
+
*/
|
|
536
|
+
const through = (cache, tools, fn) => tools.map((tool) => getOrCreate(cache, tool, () => mapTool(tool, fn)));
|
|
380
537
|
/**
|
|
381
538
|
* Tool definitions a strict server will accept, remembered per definition object.
|
|
382
539
|
*
|
|
540
|
+
* @param tools - The definitions as the pool hands them over. Never mutated — where a schema
|
|
541
|
+
* changed, a new definition is returned in its place.
|
|
542
|
+
* @returns A new array in the same order. Each entry is the definition remembered for its tool, so
|
|
543
|
+
* the same object on every call; a tool that is not a function tool is its own entry.
|
|
544
|
+
*
|
|
545
|
+
* @remarks
|
|
383
546
|
* The first call on a connection's tools does the work and every later one is a lookup, so
|
|
384
547
|
* calling this per request costs nothing.
|
|
385
|
-
*
|
|
386
|
-
* @param tools The definitions as the pool hands them over. Never mutated — where a schema
|
|
387
|
-
* changed, a new definition is returned in its place.
|
|
388
548
|
*/
|
|
389
|
-
export const sanitizeTools = (tools) => through(sanitized, tools,
|
|
549
|
+
export const sanitizeTools = (tools) => through(sanitized, tools, sanitizeSchema);
|
|
390
550
|
/**
|
|
391
551
|
* Walked as a schema rather than as arbitrary JSON, because `pattern` and `format` are keyword
|
|
392
|
-
* names and perfectly ordinary argument names at once.
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
552
|
+
* names and perfectly ordinary argument names at once.
|
|
553
|
+
*
|
|
554
|
+
* @param node - A schema position, or a list of them. Not mutated.
|
|
555
|
+
* @returns A copy without the `pattern` and `format` keywords, here and at every schema position
|
|
556
|
+
* below. A value that is neither an array nor an object comes back as it is.
|
|
557
|
+
*
|
|
558
|
+
* @remarks
|
|
559
|
+
* Matching on the key alone deleted a *property* called `format` along with the keyword, leaving
|
|
560
|
+
* the parent's `required` naming an argument that no longer existed — which every strict validator
|
|
561
|
+
* rejects, so the retry produced the failure it was reaching for. The same distinction keeps the
|
|
562
|
+
* walk out of `default`, `enum` and `const`, whose contents are data, not schema.
|
|
400
563
|
*/
|
|
401
564
|
const strip = (node) => {
|
|
402
|
-
if (Array.isArray(node))
|
|
565
|
+
if (Array.isArray(node)) {
|
|
403
566
|
return node.map(strip);
|
|
404
|
-
|
|
567
|
+
}
|
|
568
|
+
if (!isRecord(node)) {
|
|
405
569
|
return node;
|
|
570
|
+
}
|
|
406
571
|
const out = {};
|
|
407
572
|
for (const [key, value] of Object.entries(node)) {
|
|
408
|
-
if (key ===
|
|
573
|
+
if (key === 'pattern' || key === 'format') {
|
|
409
574
|
continue;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
else if (SCHEMA_MAPS.has(key) && isObject(value))
|
|
413
|
-
// The keys here are argument names; only the values are schemas.
|
|
414
|
-
out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, strip(sub)]));
|
|
415
|
-
else
|
|
416
|
-
out[key] = value;
|
|
575
|
+
}
|
|
576
|
+
out[key] = mapChildren(key, value, strip);
|
|
417
577
|
}
|
|
418
578
|
return out;
|
|
419
579
|
};
|
|
420
580
|
/**
|
|
421
|
-
*
|
|
422
|
-
* `pattern` and most `format` values, both of which only ever narrowed a string the tool
|
|
423
|
-
* re-validates anyway.
|
|
581
|
+
* One schema without its `pattern` and `format` keywords, for a schema that is not a tool's.
|
|
424
582
|
*
|
|
425
|
-
* @param
|
|
583
|
+
* @param schema - Already sanitised. Relaxing is the retry, not a substitute for `sanitizeSchema`.
|
|
584
|
+
* Never mutated; anything that is not a schema comes back as an object with no properties.
|
|
585
|
+
* @returns A new object, the keywords gone from every schema position in it.
|
|
586
|
+
*
|
|
587
|
+
* @remarks
|
|
588
|
+
* The retry `relaxTools` makes, on a bare schema. Only the keywords go: a property that happens to
|
|
589
|
+
* be called `format` is an argument name and stays.
|
|
426
590
|
*/
|
|
427
|
-
export
|
|
428
|
-
const stripped = strip(
|
|
429
|
-
return
|
|
430
|
-
}
|
|
591
|
+
export function relaxSchema(schema) {
|
|
592
|
+
const stripped = strip(schema);
|
|
593
|
+
return isRecord(stripped) ? stripped : EMPTY_OBJECT();
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* The tool array with every schema's `pattern` and `format` keywords gone — the shape a turn is
|
|
597
|
+
* retried with.
|
|
598
|
+
*
|
|
599
|
+
* @param tools - Already sanitised. Relaxing is the retry, not a substitute for `sanitizeTools`.
|
|
600
|
+
* Never mutated.
|
|
601
|
+
* @returns A new array in the same order. Each entry is the relaxed definition remembered for its
|
|
602
|
+
* tool, so the same object on every call; a tool that is not a function tool is its own entry.
|
|
603
|
+
*
|
|
604
|
+
* @remarks
|
|
605
|
+
* llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in `pattern` and most
|
|
606
|
+
* `format` values, both of which only ever narrowed a string the tool re-validates anyway.
|
|
607
|
+
*/
|
|
608
|
+
export const relaxTools = (tools) => through(relaxed, tools, relaxSchema);
|
|
431
609
|
/**
|
|
432
610
|
* Qwen chat templates raise this when the transcript has no user turn. Some servers wrap it
|
|
433
611
|
* in the same "unable to generate parser" wording as a real schema failure, and stripping
|
|
434
612
|
* keywords would not fix it.
|
|
435
613
|
*/
|
|
436
|
-
const NO_USER_QUERY =
|
|
614
|
+
const NO_USER_QUERY = 'no user query found';
|
|
437
615
|
/**
|
|
438
|
-
*
|
|
616
|
+
* Whether a failure looks like the server could not build a grammar from our tool schemas.
|
|
439
617
|
*
|
|
618
|
+
* @param message - The server's error text. Matched case-insensitively.
|
|
619
|
+
* @returns `true` for a grammar or schema-conversion failure. `false` for anything else, and
|
|
620
|
+
* always for a chat template's "no user query found", whatever it is wrapped in.
|
|
621
|
+
*
|
|
622
|
+
* @remarks
|
|
440
623
|
* Every server words this differently — llama-server says "error parsing grammar", Lemonade
|
|
441
624
|
* says "Failed to initialize samplers: failed to parse grammar", others surface the converter
|
|
442
625
|
* by name. Since a grammar is only ever involved in constrained decoding, treat any mention of
|
|
443
626
|
* one as ours; the retry is cheap and latches after a single request.
|
|
444
|
-
*
|
|
445
|
-
* @param message The server's error text. Matched case-insensitively.
|
|
446
627
|
*/
|
|
447
628
|
export function isGrammarError(message) {
|
|
448
629
|
const text = message.toLowerCase();
|
|
449
|
-
if (text.includes(NO_USER_QUERY))
|
|
630
|
+
if (text.includes(NO_USER_QUERY)) {
|
|
450
631
|
return false;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
text.includes(
|
|
454
|
-
|
|
632
|
+
}
|
|
633
|
+
return (text.includes('grammar') ||
|
|
634
|
+
text.includes('unrecognized schema') ||
|
|
635
|
+
text.includes('json schema conversion failed') ||
|
|
636
|
+
(text.includes('unable to generate parser') && text.includes('template')));
|
|
455
637
|
}
|