@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 CHANGED
@@ -61,10 +61,16 @@ function messageChars(message) {
61
61
  /**
62
62
  * The tools half, cached against the array.
63
63
  *
64
- * Tool definitions are stable objects handed out by a pool, and `sanitizeTools` already caches on
65
- * that same identity — so the array a turn sends is the array the last turn sent unless something
66
- * reconnected. Serialising two dozen JSON schemas to measure them, on every turn, to get the same
67
- * number every time, was the more expensive half of this function.
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) {
@@ -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
- * Replaces a root-level `$ref` with what it points at.
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 node = parameters;
118
- while (typeof node.$ref === "string") {
119
- const pointer = node.$ref;
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 EMPTY_OBJECT();
126
+ return undefined;
125
127
  seen.add(pointer);
126
128
  const pool = defs[target[1]];
127
- const resolved = isObject(pool) ? pool[target[2]] : undefined;
128
- if (!isObject(resolved))
129
- return EMPTY_OBJECT();
130
- node = resolved;
129
+ current = isObject(pool) ? pool[target[2]] : undefined;
131
130
  }
132
- // The definitions travel with it: whatever the target refers to still lives in them.
133
- return node === parameters ? parameters : { ...node, ...defs };
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. Branches that are references are not something to guess atthose fall through to
141
- * `pruneRequired`, which at least keeps the result self-consistent.
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 branch of branches) {
150
- if (!isObject(branch) || "$ref" in branch)
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. A branch that is a reference is not something to
174
- * guess at it cannot vouch for a name, so its presence alone empties `required`.
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 branch of branches) {
210
+ for (const raw of branches) {
186
211
  const previous = shared;
187
- if (!isObject(branch) || "$ref" in branch) {
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.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<think>[\s\S]*$/i, "");
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.5",
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",