@cubicecho/agent-core 2.0.6 → 2.0.8
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/dist/retry.js +10 -4
- package/dist/schema-compat.js +135 -30
- package/package.json +1 -1
package/dist/retry.js
CHANGED
|
@@ -61,10 +61,16 @@ function messageChars(message) {
|
|
|
61
61
|
/**
|
|
62
62
|
* The tools half, cached against the array.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
64
|
+
* Serialising two dozen JSON schemas to measure them, on every turn, to get the same number every
|
|
65
|
+
* time, was the more expensive half of this function.
|
|
66
|
+
*
|
|
67
|
+
* The array is the key, so this pays only a caller that hands the same one back. That is not what
|
|
68
|
+
* a turn built through `sanitizeTools` or `relaxTools` does — both are a `map`, so each build
|
|
69
|
+
* allocates a fresh array however stable the tools inside it are, and those builds miss here every
|
|
70
|
+
* time. It is not free to change: keying on the tools instead would hit for them, at the cost of
|
|
71
|
+
* the array-level memoisation `tests/retry.test.ts` pins, which deliberately holds a mutated array
|
|
72
|
+
* to its first reading. Sizing happens once per turn either way, so the miss costs one walk of the
|
|
73
|
+
* schemas rather than a walk per attempt.
|
|
68
74
|
*/
|
|
69
75
|
const toolTokens = new WeakMap();
|
|
70
76
|
function toolsCost(tools) {
|
package/dist/schema-compat.js
CHANGED
|
@@ -99,55 +99,76 @@ function collapseNullableUnion(node) {
|
|
|
99
99
|
const TOP_LEVEL_COMBINATORS = ["allOf", "anyOf", "oneOf", "enum", "not"];
|
|
100
100
|
/** `#/definitions/Args` or `#/$defs/Args` — a pointer into this schema's own definitions. */
|
|
101
101
|
const LOCAL_POINTER = /^#\/(definitions|\$defs)\/([^/]+)$/;
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
* Dropping the siblings of a `$ref` is right at a nested position and wrong at this one: the
|
|
106
|
-
* siblings here are the `definitions` the pointer needs, so the reference is left dangling and
|
|
107
|
-
* `properties` is then backfilled empty below. The tool goes out advertising no arguments at
|
|
108
|
-
* all — which the model cannot detect and the server has no reason to refuse. A schema
|
|
109
|
-
* generator emits this shape whenever the argument object is a named type.
|
|
110
|
-
*/
|
|
111
|
-
function inlineRootRef(parameters) {
|
|
102
|
+
/** The `definitions` and `$defs` that a local pointer in this schema resolves against. */
|
|
103
|
+
function poolsOf(parameters) {
|
|
112
104
|
const defs = {};
|
|
113
105
|
for (const key of ["definitions", "$defs"])
|
|
114
106
|
if (isObject(parameters[key]))
|
|
115
107
|
defs[key] = parameters[key];
|
|
108
|
+
return defs;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Follows a chain of local references to the schema it arrives at, or `undefined` where it
|
|
112
|
+
* arrives at none — a pointer into another document, one that comes back around to itself, or a
|
|
113
|
+
* name the pools do not hold. A node that is not a reference resolves to itself, so a caller can
|
|
114
|
+
* hand this a branch without first asking which spelling it is.
|
|
115
|
+
*
|
|
116
|
+
* @param node The schema position to resolve, reference or not.
|
|
117
|
+
* @param defs The pools to resolve against, as `poolsOf` collects them from the root.
|
|
118
|
+
*/
|
|
119
|
+
function resolveRef(node, defs) {
|
|
116
120
|
const seen = new Set();
|
|
117
|
-
let
|
|
118
|
-
while (typeof
|
|
119
|
-
const pointer =
|
|
121
|
+
let current = node;
|
|
122
|
+
while (isObject(current) && typeof current.$ref === "string") {
|
|
123
|
+
const pointer = current.$ref;
|
|
120
124
|
const target = LOCAL_POINTER.exec(pointer);
|
|
121
|
-
// A pointer at another document, or one that comes back to itself, has nothing here to
|
|
122
|
-
// resolve against. An object with no properties is at least honest about taking none.
|
|
123
125
|
if (!target || seen.has(pointer))
|
|
124
|
-
return
|
|
126
|
+
return undefined;
|
|
125
127
|
seen.add(pointer);
|
|
126
128
|
const pool = defs[target[1]];
|
|
127
|
-
|
|
128
|
-
if (!isObject(resolved))
|
|
129
|
-
return EMPTY_OBJECT();
|
|
130
|
-
node = resolved;
|
|
129
|
+
current = isObject(pool) ? pool[target[2]] : undefined;
|
|
131
130
|
}
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
return isObject(current) ? current : undefined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Replaces a root-level `$ref` with what it points at.
|
|
135
|
+
*
|
|
136
|
+
* Dropping the siblings of a `$ref` is right at a nested position and wrong at this one: the
|
|
137
|
+
* siblings here are the `definitions` the pointer needs, so the reference is left dangling and
|
|
138
|
+
* `properties` is then backfilled empty below. The tool goes out advertising no arguments at
|
|
139
|
+
* all — which the model cannot detect and the server has no reason to refuse. A schema
|
|
140
|
+
* generator emits this shape whenever the argument object is a named type.
|
|
141
|
+
*/
|
|
142
|
+
function inlineRootRef(parameters) {
|
|
143
|
+
if (typeof parameters.$ref !== "string")
|
|
144
|
+
return parameters;
|
|
145
|
+
const defs = poolsOf(parameters);
|
|
146
|
+
const resolved = resolveRef(parameters, defs);
|
|
147
|
+
// A pointer that lands nowhere has nothing here to resolve against. An object with no
|
|
148
|
+
// properties is at least honest about taking none.
|
|
149
|
+
// The definitions travel with what it did land on: whatever that refers to still lives in them.
|
|
150
|
+
return resolved ? { ...resolved, ...defs } : EMPTY_OBJECT();
|
|
134
151
|
}
|
|
135
152
|
/**
|
|
136
153
|
* Folds a root `allOf` into the root itself.
|
|
137
154
|
*
|
|
138
155
|
* It is the other way a generated schema spells "the arguments are this named type", and
|
|
139
156
|
* deleting it outright below threw the arguments away while leaving the `required` that named
|
|
140
|
-
* them.
|
|
141
|
-
* `
|
|
157
|
+
* them. A branch is far more often a reference than an inline object — a named type is exactly
|
|
158
|
+
* what a generator puts in `$defs` — so each is resolved against this schema's own pools first.
|
|
159
|
+
* One that resolves nowhere is not something to guess at, and falls through to `pruneRequired`,
|
|
160
|
+
* which at least keeps the result self-consistent.
|
|
142
161
|
*/
|
|
143
162
|
function mergeRootAllOf(out) {
|
|
144
163
|
const branches = out.allOf;
|
|
145
164
|
if (!Array.isArray(branches))
|
|
146
165
|
return;
|
|
166
|
+
const defs = poolsOf(out);
|
|
147
167
|
const properties = isObject(out.properties) ? { ...out.properties } : {};
|
|
148
168
|
const required = new Set(Array.isArray(out.required) ? out.required.filter((name) => typeof name === "string") : []);
|
|
149
|
-
for (const
|
|
150
|
-
|
|
169
|
+
for (const raw of branches) {
|
|
170
|
+
const branch = resolveRef(raw, defs);
|
|
171
|
+
if (!branch)
|
|
151
172
|
continue;
|
|
152
173
|
if (isObject(branch.properties))
|
|
153
174
|
Object.assign(properties, branch.properties);
|
|
@@ -170,10 +191,14 @@ function mergeRootAllOf(out) {
|
|
|
170
191
|
* `collapseNullableUnion` to take apart, so it reached the delete below intact and every
|
|
171
192
|
* argument went with it. Properties are unioned because a caller satisfies any one branch;
|
|
172
193
|
* `required` keeps only the names every branch asks for, since one that a branch does without
|
|
173
|
-
* is one the model has to be free to omit.
|
|
174
|
-
*
|
|
194
|
+
* is one the model has to be free to omit. The branches of a discriminated union arrive as
|
|
195
|
+
* references rather than inline — Pydantic, zod-to-json-schema and the MCP TypeScript SDK all
|
|
196
|
+
* emit the shapes into `$defs` and point at them from the root — so each is resolved against
|
|
197
|
+
* this schema's own pools first. One that resolves nowhere still cannot vouch for a name, so its
|
|
198
|
+
* presence alone empties `required`.
|
|
175
199
|
*/
|
|
176
200
|
function mergeRootUnion(out) {
|
|
201
|
+
const defs = poolsOf(out);
|
|
177
202
|
for (const key of ["anyOf", "oneOf"]) {
|
|
178
203
|
const branches = out[key];
|
|
179
204
|
if (!Array.isArray(branches))
|
|
@@ -182,9 +207,10 @@ function mergeRootUnion(out) {
|
|
|
182
207
|
// `null` until a branch has been read, which is what tells "no branches yet" apart from
|
|
183
208
|
// "the branches agreed on nothing".
|
|
184
209
|
let shared = null;
|
|
185
|
-
for (const
|
|
210
|
+
for (const raw of branches) {
|
|
186
211
|
const previous = shared;
|
|
187
|
-
|
|
212
|
+
const branch = resolveRef(raw, defs);
|
|
213
|
+
if (!branch) {
|
|
188
214
|
shared = new Set();
|
|
189
215
|
continue;
|
|
190
216
|
}
|
|
@@ -206,6 +232,84 @@ function mergeRootUnion(out) {
|
|
|
206
232
|
}
|
|
207
233
|
}
|
|
208
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Every `$ref` string anywhere under a node, walked as arbitrary JSON rather than as a schema.
|
|
237
|
+
*
|
|
238
|
+
* The keyword-aware walk `strip` does is the wrong way round for this one. Missing a pointer
|
|
239
|
+
* here means deleting a definition that something still refers to, which breaks the schema;
|
|
240
|
+
* finding one that was really a string sitting in a `default` or an `enum` costs a definition
|
|
241
|
+
* that outlives its last real reference. So this errs the cheap way and reads every position.
|
|
242
|
+
*/
|
|
243
|
+
function collectRefs(node, into) {
|
|
244
|
+
if (Array.isArray(node)) {
|
|
245
|
+
for (const item of node)
|
|
246
|
+
collectRefs(item, into);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!isObject(node))
|
|
250
|
+
return;
|
|
251
|
+
for (const [key, value] of Object.entries(node)) {
|
|
252
|
+
if (key === "$ref" && typeof value === "string")
|
|
253
|
+
into.add(value);
|
|
254
|
+
else
|
|
255
|
+
collectRefs(value, into);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Drops the `definitions` and `$defs` entries that nothing points at any more.
|
|
260
|
+
*
|
|
261
|
+
* The rewrites above delete whole subtrees — a root combinator once its branches are folded in,
|
|
262
|
+
* every sibling of a `$ref`, the branch of a union that was only ever `null` — and the pointers
|
|
263
|
+
* go with them while the pools they named stay behind. On a real Gmail or filesystem schema
|
|
264
|
+
* those pools are most of the parameter bytes, re-sent for every tool on every turn of every
|
|
265
|
+
* run, describing shapes the request no longer mentions anywhere.
|
|
266
|
+
*
|
|
267
|
+
* Reachability rather than a single pass, because a definition that is still pointed at can
|
|
268
|
+
* name another; a cycle among them terminates on the `has` check, whether or not anything
|
|
269
|
+
* outside it still refers in.
|
|
270
|
+
*/
|
|
271
|
+
function pruneDefs(out) {
|
|
272
|
+
const pools = ["definitions", "$defs"].filter((key) => isObject(out[key]));
|
|
273
|
+
if (!pools.length)
|
|
274
|
+
return;
|
|
275
|
+
const live = {};
|
|
276
|
+
const visit = (node) => {
|
|
277
|
+
const pointers = new Set();
|
|
278
|
+
collectRefs(node, pointers);
|
|
279
|
+
for (const pointer of pointers) {
|
|
280
|
+
const target = LOCAL_POINTER.exec(pointer);
|
|
281
|
+
if (!target)
|
|
282
|
+
continue;
|
|
283
|
+
const [, poolKey, name] = target;
|
|
284
|
+
const pool = out[poolKey];
|
|
285
|
+
if (!isObject(pool) || !(name in pool))
|
|
286
|
+
continue;
|
|
287
|
+
live[poolKey] ??= new Set();
|
|
288
|
+
const names = live[poolKey];
|
|
289
|
+
if (names.has(name))
|
|
290
|
+
continue;
|
|
291
|
+
names.add(name);
|
|
292
|
+
visit(pool[name]);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
// The pools themselves are not roots: a definition is reached from the schema body, or by
|
|
296
|
+
// another definition that was, or not at all.
|
|
297
|
+
const body = { ...out };
|
|
298
|
+
for (const key of pools)
|
|
299
|
+
delete body[key];
|
|
300
|
+
visit(body);
|
|
301
|
+
for (const key of pools) {
|
|
302
|
+
const names = live[key];
|
|
303
|
+
if (!names?.size) {
|
|
304
|
+
delete out[key];
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const pool = out[key];
|
|
308
|
+
if (names.size === Object.keys(pool).length)
|
|
309
|
+
continue;
|
|
310
|
+
out[key] = Object.fromEntries(Object.entries(pool).filter(([name]) => names.has(name)));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
209
313
|
/**
|
|
210
314
|
* A required argument that is not in `properties` is one no caller can supply and no strict
|
|
211
315
|
* validator will accept. Anything the rewrites above removed, `required` may still name.
|
|
@@ -233,6 +337,7 @@ function sanitizeParameters(parameters) {
|
|
|
233
337
|
if (!isObject(out.properties))
|
|
234
338
|
out.properties = {};
|
|
235
339
|
pruneRequired(out);
|
|
340
|
+
pruneDefs(out);
|
|
236
341
|
return out;
|
|
237
342
|
}
|
|
238
343
|
/** Rewrites one tool's parameters, leaving a non-function tool alone. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openai",
|