@cubicecho/agent-core 2.0.5 → 2.0.7
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 +56 -30
- package/dist/side-task.js +10 -1
- 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
|
}
|
package/dist/side-task.js
CHANGED
|
@@ -61,7 +61,16 @@ function rejectedTheRequest(error) {
|
|
|
61
61
|
* model that spends it deliberating is cut off mid-scratchpad and the closing tag never
|
|
62
62
|
* arrives — and the whole deliberation was then returned to the caller as the answer.
|
|
63
63
|
*/
|
|
64
|
-
const stripThinking = (text) => text
|
|
64
|
+
const stripThinking = (text) => text
|
|
65
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
66
|
+
.replace(/<think>[\s\S]*$/i, "")
|
|
67
|
+
// And the fence that never opens. Several chat templates put the opening tag at the end of
|
|
68
|
+
// the prompt rather than leaving the model to write it, so what comes back is deliberation
|
|
69
|
+
// first and only the closing tag to mark where it stops. Neither pattern above matches that,
|
|
70
|
+
// and the whole scratchpad went to the caller as the answer — a session title, a tool
|
|
71
|
+
// preselection, a suggestion list. Every real `<think>` is gone by this point, so a `</think>`
|
|
72
|
+
// still here opened in the prompt; the first one is taken, which keeps the most text.
|
|
73
|
+
.replace(/^[\s\S]*?<\/think>/i, "");
|
|
65
74
|
/**
|
|
66
75
|
* Runs a side task and returns the reply text, thinking stripped. Throws like any request.
|
|
67
76
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
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",
|