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