@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.
Files changed (71) hide show
  1. package/README.md +10 -7
  2. package/dist/agent-loop.d.ts +84 -205
  3. package/dist/agent-loop.js +288 -597
  4. package/dist/calibration.d.ts +14 -10
  5. package/dist/calibration.js +30 -23
  6. package/dist/capabilities.d.ts +111 -42
  7. package/dist/capabilities.js +281 -138
  8. package/dist/client.d.ts +104 -35
  9. package/dist/client.js +205 -74
  10. package/dist/compaction.d.ts +103 -60
  11. package/dist/compaction.js +160 -86
  12. package/dist/config.d.ts +48 -12
  13. package/dist/config.js +20 -1
  14. package/dist/continuation.d.ts +24 -16
  15. package/dist/continuation.js +70 -44
  16. package/dist/digest.d.ts +7 -0
  17. package/dist/digest.js +18 -0
  18. package/dist/errors.d.ts +24 -15
  19. package/dist/errors.js +23 -15
  20. package/dist/events.d.ts +100 -63
  21. package/dist/events.js +250 -120
  22. package/dist/guards.d.ts +49 -4
  23. package/dist/guards.js +60 -5
  24. package/dist/hook-events.d.ts +21 -5
  25. package/dist/hook-events.js +23 -9
  26. package/dist/hooks.d.ts +96 -73
  27. package/dist/hooks.js +202 -116
  28. package/dist/index.d.ts +30 -26
  29. package/dist/index.js +29 -24
  30. package/dist/ledger.d.ts +62 -30
  31. package/dist/ledger.js +163 -56
  32. package/dist/platform.d.ts +15 -0
  33. package/dist/platform.js +15 -0
  34. package/dist/preselect.d.ts +223 -0
  35. package/dist/preselect.js +291 -0
  36. package/dist/request-body.d.ts +32 -0
  37. package/dist/request-body.js +47 -0
  38. package/dist/reset.d.ts +1 -0
  39. package/dist/reset.js +7 -6
  40. package/dist/retry.d.ts +44 -138
  41. package/dist/retry.js +83 -281
  42. package/dist/run-calls.d.ts +123 -0
  43. package/dist/run-calls.js +351 -0
  44. package/dist/run-turn.d.ts +36 -15
  45. package/dist/run-turn.js +74 -37
  46. package/dist/runtime.d.ts +23 -17
  47. package/dist/runtime.js +34 -24
  48. package/dist/schema-compat.d.ts +35 -18
  49. package/dist/schema-compat.js +346 -176
  50. package/dist/scope.d.ts +28 -5
  51. package/dist/scope.js +37 -10
  52. package/dist/side-task.d.ts +124 -40
  53. package/dist/side-task.js +186 -66
  54. package/dist/snapshot.d.ts +10 -5
  55. package/dist/snapshot.js +73 -30
  56. package/dist/spec.d.ts +56 -25
  57. package/dist/spec.js +494 -215
  58. package/dist/stream.d.ts +66 -16
  59. package/dist/stream.js +220 -97
  60. package/dist/thinking.d.ts +23 -8
  61. package/dist/thinking.js +92 -44
  62. package/dist/tokens.d.ts +164 -6
  63. package/dist/tokens.js +319 -6
  64. package/dist/tool-calls.d.ts +90 -17
  65. package/dist/tool-calls.js +207 -84
  66. package/dist/tool-loading.d.ts +101 -189
  67. package/dist/tool-loading.js +257 -371
  68. package/dist/wire.d.ts +81 -0
  69. package/dist/wire.js +73 -0
  70. package/llms.txt +76 -39
  71. package/package.json +4 -4
@@ -1,150 +1,222 @@
1
- import { isRecord } from "./guards.js";
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(["object", "string", "number", "integer", "boolean", "array", "null"]);
5
- const EMPTY_OBJECT = () => ({ type: "object", properties: {} });
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
- "items",
9
- "additionalProperties",
10
- "not",
11
- "if",
12
- "then",
13
- "else",
14
- "contains",
15
- "propertyNames",
16
- "anyOf",
17
- "oneOf",
18
- "allOf",
19
- "prefixItems",
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(["properties", "patternProperties", "$defs", "definitions"]);
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 = ["definitions", "$defs"];
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
- * Coerces one schema position. Malformed MCP output sometimes puts a bare type name where a
43
- * whole schema belongs, which the grammar converter reports as `Unrecognized schema: "object"`.
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 === "string")
47
- return PRIMITIVES.has(node) && node !== "object" ? { type: node } : EMPTY_OBJECT();
48
- if (typeof node === "boolean")
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
- if (!isRecord(node))
76
+ }
77
+ if (!isRecord(node)) {
51
78
  return EMPTY_OBJECT();
79
+ }
52
80
  return normalize(node);
53
81
  }
54
- /** Recursively rewrites the shapes llama.cpp's grammar converter cannot represent. */
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 out = {};
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 === "type" && Array.isArray(value)) {
60
- const names = value.filter((item) => typeof item === "string");
61
- const concrete = names.filter((name) => name !== "null");
62
- if (names.includes("null"))
63
- out.nullable = true;
64
- if (concrete.length === 1)
65
- out.type = concrete[0];
66
- else if (concrete.length > 1)
67
- out.anyOf = concrete.map((name) => ({ type: name }));
68
- else
69
- out.type = "null";
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
- out[key] = mapChildren(key, value, asSchema);
110
+ built[key] = mapChildren(key, value, asSchema);
73
111
  }
74
112
  }
75
- collapseNullableUnion(out);
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 === "string" && LOOKAROUND.test(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 === "object" && !isRecord(out.properties))
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 ("$ref" in out)
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. Optionality already lives in the parent's `required`, so keep the one real
96
- * branch. A union with two real branches is meaningful and is left alone.
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
- for (const key of ["anyOf", "oneOf"]) {
100
- const variants = node[key];
101
- if (!Array.isArray(variants))
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
- const concrete = variants.filter((item) => !(isRecord(item) && item.type === "null"));
104
- if (concrete.length !== 1 || concrete.length === variants.length)
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
- delete node[key];
107
- Object.assign(node, { nullable: true, ...(isRecord(concrete[0]) ? concrete[0] : {}) });
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 = ["allOf", "anyOf", "oneOf", "enum", "not"];
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
- /** The `definitions` and `$defs` that a local pointer in this schema resolves against. */
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. A node that is not a reference resolves to itself, so a caller can
126
- * hand this a branch without first asking which spelling it is.
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
- * @param node The schema position to resolve, reference or not.
129
- * @param defs The pools to resolve against, as `poolsOf` collects them from the root.
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 === "string") {
199
+ while (isRecord(current) && typeof current.$ref === 'string') {
135
200
  const pointer = current.$ref;
136
- const target = LOCAL_POINTER.exec(pointer);
137
- if (!target || seen.has(pointer))
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[target[1]];
141
- current = isRecord(pool) ? pool[target[2]] : undefined;
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 !== "string")
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(out) {
175
- const branches = out.allOf;
176
- if (!Array.isArray(branches))
177
- return;
178
- const defs = poolsOf(out);
179
- const properties = isRecord(out.properties) ? { ...out.properties } : {};
180
- const required = new Set(requiredOf(out));
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
- if (isRecord(branch.properties))
265
+ }
266
+ if (isRecord(branch.properties)) {
186
267
  Object.assign(properties, branch.properties);
187
- for (const name of requiredOf(branch))
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
- out.properties = properties;
193
- if (required.size)
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(out) {
211
- const defs = poolsOf(out);
212
- for (const key of ["anyOf", "oneOf"]) {
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
- out.properties = { ...(isRecord(out.properties) ? out.properties : {}), ...properties };
235
- if (shared?.size)
236
- out.required = [...new Set([...requiredOf(out), ...shared])];
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 === "$ref" && typeof value === "string")
357
+ if (key === '$ref' && typeof value === 'string') {
257
358
  into.add(value);
258
- else
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(out) {
276
- const pools = POOL_KEYS.filter((key) => isRecord(out[key]));
277
- if (!pools.length)
278
- return;
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 = out[poolKey];
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 = { ...out };
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
- for (const key of pools) {
424
+ let pruned = schema;
425
+ for (const [key, pool] of pools) {
306
426
  const names = live[key];
307
427
  if (!names?.size) {
308
- delete out[key];
428
+ const { [key]: _unreached, ...rest } = pruned;
429
+ pruned = rest;
309
430
  continue;
310
431
  }
311
- const pool = out[key];
312
- if (names.size === Object.keys(pool).length)
432
+ if (names.size === Object.keys(pool).length) {
313
433
  continue;
314
- out[key] = Object.fromEntries(Object.entries(pool).filter(([name]) => names.has(name)));
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. Anything the rewrites above removed, `required` may still name.
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(out) {
322
- if (!Array.isArray(out.required))
323
- return;
324
- const properties = isRecord(out.properties) ? out.properties : {};
325
- const kept = out.required.filter((name) => typeof name === "string" && name in properties);
326
- if (kept.length)
327
- out.required = kept;
328
- else
329
- delete out.required;
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
- const out = normalize(inlineRootRef(schema));
347
- mergeRootAllOf(out);
348
- mergeRootUnion(out);
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
- if (out.type !== "object")
352
- out.type = "object";
353
- if (!isRecord(out.properties))
486
+ }
487
+ if (out.type !== SchemaType.Object) {
488
+ out.type = SchemaType.Object;
489
+ }
490
+ if (!isRecord(out.properties)) {
354
491
  out.properties = {};
355
- pruneRequired(out);
356
- pruneDefs(out);
357
- return out;
492
+ }
493
+ return pruneDefs(pruneRequired(out));
358
494
  }
359
- /** Rewrites one tool's parameters, leaving a non-function tool alone. */
360
- const mapTool = (tool, fn) => tool.type === "function"
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
- /** Looks one up, computing and remembering it on a miss. */
388
- const through = (cache, tools, fn) => tools.map((tool) => {
389
- const hit = cache.get(tool);
390
- if (hit)
391
- return hit;
392
- const built = mapTool(tool, fn);
393
- cache.set(tool, built);
394
- return built;
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. Matching on the key alone deleted a
409
- * *property* called `format` along with the keyword, leaving the parent's `required` naming an
410
- * argument that no longer existed — which every strict validator rejects, so the retry produced
411
- * the failure it was reaching for. The same distinction keeps the walk out of `default`, `enum`
412
- * and `const`, whose contents are data, not schema.
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
- if (!isRecord(node))
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 === "pattern" || key === "format")
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 retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
442
- * `pattern` and most `format` values, both of which only ever narrowed a string the tool
443
- * re-validates anyway.
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
- * @param tools Already sanitised. Relaxing is the retry, not a substitute for `sanitizeTools`.
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 = "no user query found";
619
+ const NO_USER_QUERY = 'no user query found';
454
620
  /**
455
- * Does this failure look like the server could not build a grammar from our tool schemas?
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
- return (text.includes("grammar") ||
469
- text.includes("unrecognized schema") ||
470
- text.includes("json schema conversion failed") ||
471
- (text.includes("unable to generate parser") && text.includes("template")));
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
  }