@juno-ai/bind 11.0.0 → 13.0.0
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/package.json +1 -1
- package/tools/sanitize-schema.d.ts +194 -0
- package/tools/sanitize-schema.js +1041 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juno-ai/bind",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.0.0",
|
|
4
4
|
"description": "Agent harness: the tool-calling turn kernel, deterministic LLM provider routing with transport-error classification, the streaming-completion watchdog, run mechanics, sub-agent lineage and admission, transcript healing, tool-schema sanitization, the plugin/tool vocabulary, and the skill vocabulary (`./skills`) for progressive knowledge disclosure. MIT-licensed; published to npm from the canonical repo via scripts/publish-bind.ts (docs/bind.md).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -100,6 +100,196 @@
|
|
|
100
100
|
* `properties` is an open per-database map with no fixed sub-schema), and the
|
|
101
101
|
* key name is never changed so the model still emits the right argument key.
|
|
102
102
|
* Confirmed live: `x-ai/grok-4.3` 400→200, Gemini/Claude unaffected.
|
|
103
|
+
* 8. Resolve a COMPOSITION BRANCH's `required` against the branch's own
|
|
104
|
+
* `properties` UNIONED with the enclosing node's `properties`, and — at the
|
|
105
|
+
* parameters ROOT only — make the surviving branch self-sufficient by copying
|
|
106
|
+
* the referenced property subschemas down into it and setting
|
|
107
|
+
* `type: "object"`. This is the narrow, measured exception to transform #3.
|
|
108
|
+
* The ordinary way to say "pass exactly one of these" puts the properties on
|
|
109
|
+
* the parent and only `required` in each branch:
|
|
110
|
+
*
|
|
111
|
+
* { type: "object", properties: { query: …, memoryItemId: … },
|
|
112
|
+
* oneOf: [ { required: ["query"] }, { required: ["memoryItemId"] } ] }
|
|
113
|
+
*
|
|
114
|
+
* which is how JSON Schema composition works, and which transform #3 alone
|
|
115
|
+
* sanitized to `oneOf: [{}, {}]` — a root xAI (Grok) hard-rejects
|
|
116
|
+
* (`tool parameter root must be an object type (root schema is an
|
|
117
|
+
* anyOf/oneOf union with a non-object branch)`, failing EVERY tool in the
|
|
118
|
+
* request), and which Gemini/OpenAI accept while silently losing the
|
|
119
|
+
* constraint, letting the model send both parameters or neither. Measured on
|
|
120
|
+
* xAI for branches of a ROOT union: `{type:"object", …}` or `{properties:…}`
|
|
121
|
+
* is accepted with EITHER alone sufficing; a bare `{required:[…]}` or `{}` is
|
|
122
|
+
* a 400. Hence the copy-down, which also makes the branch idempotent under
|
|
123
|
+
* this sanitizer (a second pass finds every `required` name in the branch's
|
|
124
|
+
* OWN `properties` and changes nothing).
|
|
125
|
+
*
|
|
126
|
+
* Two limits are load-bearing, both measured:
|
|
127
|
+
* - The parent scope is the ENCLOSING node only, and only for
|
|
128
|
+
* `allOf`/`anyOf`/`oneOf` elements. Widening transform #3 to resolve
|
|
129
|
+
* `required` against ALL ancestors at every node reintroduces the hard
|
|
130
|
+
* Gemini 400 that transform #3 exists to prevent ("required fields
|
|
131
|
+
* ['value'] are not defined in the schema properties" — still true on
|
|
132
|
+
* gemini-3.5/3.6/3.7-flash). Every non-branch node keeps transform #3
|
|
133
|
+
* exactly as it is, and so do `if`/`then`/`else` and `dependentSchemas`,
|
|
134
|
+
* which are not composition arrays.
|
|
135
|
+
* - The copy-down is ROOT-ONLY. The identical bare-`required` union nested
|
|
136
|
+
* under a property is accepted by all nine measured models including
|
|
137
|
+
* every Grok version, so a nested composition keeps its `required` (that
|
|
138
|
+
* is the fix) and is otherwise left alone — no provider constrains it,
|
|
139
|
+
* and churning it risks the transform-#5 `allOf` flatten.
|
|
140
|
+
* Only the names in the branch's own `required` are copied, so a branch does
|
|
141
|
+
* not inherit the parent's whole property set — and a single ceiling
|
|
142
|
+
* (`MAX_ROOT_COPY_DOWN`) bounds the copy-down across ALL branches of one
|
|
143
|
+
* root composition, because the per-branch bound does not bound N branches
|
|
144
|
+
* × P names. Past the ceiling the repair is skipped, leaving the branches
|
|
145
|
+
* bare for rule (a) below to drop.
|
|
146
|
+
* 9. Guarantee the parameters ROOT is one xAI will accept, as a final pass.
|
|
147
|
+
* xAI rejects a root that is not an object type, and the rejection takes the
|
|
148
|
+
* whole request (every tool) with it. Two distinct rules, each measured
|
|
149
|
+
* against grok-4.3/4.5/4.6 (and confirmed inert on the six Gemini/OpenAI
|
|
150
|
+
* models) through OpenRouter with `provider.allow_fallbacks: false` and one
|
|
151
|
+
* tool per request:
|
|
152
|
+
*
|
|
153
|
+
* a. Every element of a root `anyOf`/`oneOf` must be a plain object that
|
|
154
|
+
* is object-shaped, and must not be `{}`. A branch that declares a
|
|
155
|
+
* `type` is judged on that alone (`type: "object"` passes, anything
|
|
156
|
+
* else fails even when the branch also carries `properties` — measured
|
|
157
|
+
* 0/3, identical to a bare `{type:"string"}`); a branch with no `type`
|
|
158
|
+
* passes on `properties`. This holds even when the root ITSELF declares
|
|
159
|
+
* `type: "object"` and `properties` — an object root does not excuse a
|
|
160
|
+
* non-object branch. Rule (a) runs BEFORE the transform-#5 flatten,
|
|
161
|
+
* since that flatten refuses to merge an `allOf` while a union sibling
|
|
162
|
+
* is present; dropping the union afterwards would strand an `allOf`
|
|
163
|
+
* that the next pass would merge, breaking idempotence. A
|
|
164
|
+
* branch that still fails after #8 cannot be repaired (its `required`
|
|
165
|
+
* names something no `properties` map declares — the parent supplies it
|
|
166
|
+
* only via `patternProperties`/`additionalProperties`, or not at all —
|
|
167
|
+
* or it is a `$ref`/scalar/boolean branch), so the whole keyword is
|
|
168
|
+
* dropped. Losing one tool's constraint is strictly better than losing
|
|
169
|
+
* every tool in the request.
|
|
170
|
+
* b. A root carrying a composition keyword but declaring neither `type`
|
|
171
|
+
* nor `properties` is rejected on its OWN account, whatever the branches
|
|
172
|
+
* look like: `{allOf:[{$ref:…}], $defs:…}` and
|
|
173
|
+
* `{allOf:[{type:"object",…}, {not:…}]}` are both 0/3 on Grok. Adding
|
|
174
|
+
* `type: "object"` to such a root makes exactly those cases pass 9/9
|
|
175
|
+
* without touching the composition — so the un-flattenable root `allOf`
|
|
176
|
+
* that transform #5 deliberately preserves is RESCUED rather than
|
|
177
|
+
* discarded, and it stops being the silent 400 it is today. A non-object
|
|
178
|
+
* `type` the root declared for itself IS rewritten to `"object"` and
|
|
179
|
+
* recorded in the description — measured, such a root is rejected by
|
|
180
|
+
* every model. Nested nodes keep their declared types unless transform
|
|
181
|
+
* #12 applies.
|
|
182
|
+
*
|
|
183
|
+
* Rule (a) is all-or-nothing per keyword, because ONE unusable branch
|
|
184
|
+
* poisons the whole union on xAI even when its siblings are good (0/3 with a
|
|
185
|
+
* `{type:"string"}` sibling next to a valid object branch). Filtering the bad
|
|
186
|
+
* branches out instead is accepted by xAI (3/3) but is NOT what we do — it
|
|
187
|
+
* advertises a narrower tool, telling the model an arm is invalid so it never
|
|
188
|
+
* calls that shape, with nothing to surface the loss. Dropping the keyword
|
|
189
|
+
* leaves the parent's `properties` fully visible, so every shape stays
|
|
190
|
+
* callable and a wrong COMBINATION comes back from dispatch/the remote
|
|
191
|
+
* server as a recoverable tool error.
|
|
192
|
+
*
|
|
193
|
+
* A branch's OWN `anyOf`/`oneOf` disqualifies it too, whatever else it
|
|
194
|
+
* declares: `{type:"object", anyOf:[…]}` as a root union branch is 0/3 on
|
|
195
|
+
* Grok, as is a branch carrying both `allOf` and `anyOf` — the union is what
|
|
196
|
+
* poisons it. Scope is the branch's own keys and never its subtree: a
|
|
197
|
+
* composition on a branch's PROPERTY is accepted 9/9, one or two levels
|
|
198
|
+
* down, so a subtree scan would destroy constraints every provider honours.
|
|
199
|
+
* A branch's own `allOf` is likewise accepted 9/9 (even with a
|
|
200
|
+
* bare-`required` sub-branch), the same intersection-vs-union asymmetry xAI
|
|
201
|
+
* applies to the root — hence `ROOT_UNION_KEYS`, not `COMPOSITION_KEYS`.
|
|
202
|
+
*
|
|
203
|
+
* A root union of `$ref` branches is no longer a loss — transform #14 below
|
|
204
|
+
* inlines the targets so the union survives. What remains is the narrow
|
|
205
|
+
* residue it cannot inline: a target that is not a plain object schema, one
|
|
206
|
+
* whose expanded size exceeds the copy-down ceiling, and a self-referential
|
|
207
|
+
* root pointer. Those branches stay unusable and still drop the keyword.
|
|
208
|
+
*
|
|
209
|
+
* Rule (b) covers EVERY root, not only composition roots. A property sweep
|
|
210
|
+
* over recursive shapes (`__tests__/tool-schema/property/`) found the
|
|
211
|
+
* narrower gate emitting unusable roots for whole classes the fixtures
|
|
212
|
+
* never reached: a scalar or array root (reachable from transform #1
|
|
213
|
+
* collapsing `{type:["string","null"]}`), an annotation-only root, a
|
|
214
|
+
* `$defs`-only root, and a root whose `type` contradicts its `properties` —
|
|
215
|
+
* that last one rejected by all nine models, the only rule here with no
|
|
216
|
+
* tolerant provider. Rule (b) also DROPS a `const` or `$ref` carried by the
|
|
217
|
+
* root: measured `root schema is a const` / `root schema is a $ref`, 400
|
|
218
|
+
* even alongside `type: "object"` and `properties`, and for `$ref` even
|
|
219
|
+
* when the target resolves. Nested `const`/`$ref` are meaningful and
|
|
220
|
+
* untouched.
|
|
221
|
+
*
|
|
222
|
+
* A root `allOf`'s BRANCHES are deliberately not constrained by (a):
|
|
223
|
+
* measured, xAI accepts `$ref`, `not`, nested-`allOf` and even scalar
|
|
224
|
+
* branches under an `allOf` as long as (b) holds — only `anyOf`/`oneOf`
|
|
225
|
+
* branches are validated individually. A bare union root whose branches are
|
|
226
|
+
* all object-carrying is accepted too (xAI infers object-ness from them), so
|
|
227
|
+
* (b) leaves it alone. Compositions nested below the root are untouched.
|
|
228
|
+
*
|
|
229
|
+
* 10. Drop an EMPTY array-valued keyword (`allOf`/`anyOf`/`oneOf`/`prefixItems`)
|
|
230
|
+
* at any depth. xAI rejects one anywhere in the tree — `/properties/a/oneOf:
|
|
231
|
+
* [] has less than 1 item` — and it carries no constraint (an `allOf` of
|
|
232
|
+
* nothing is satisfied by everything). Empty OBJECTS (`properties: {}`,
|
|
233
|
+
* `$defs: {}`) and an empty `required: []` are accepted and left alone;
|
|
234
|
+
* `enum: []` and `required: []` were already omitted by transforms #2/#3.
|
|
235
|
+
* 11. Drop a `$ref` whose LOCAL target does not resolve. xAI resolves `#/…`
|
|
236
|
+
* pointers itself and 400s the whole request on a dangling one
|
|
237
|
+
* (`unresolvable $ref '#/$defs/Nope'`), which a third-party MCP server
|
|
238
|
+
* produces easily by shipping a subschema without its definitions — or by
|
|
239
|
+
* putting `$defs` on a nested node, since `#/$defs/X` is anchored at the
|
|
240
|
+
* document ROOT and a nested `$defs` is unaddressable that way. External
|
|
241
|
+
* refs (`https://…`) and resolvable non-`$defs` pointers (`#/properties/a`)
|
|
242
|
+
* are both accepted by xAI and left alone. Only the `$ref` keyword is
|
|
243
|
+
* dropped, so the rest of the node survives.
|
|
244
|
+
*
|
|
245
|
+
* 12. Give any node carrying `properties` an explicit `type: "object"`. Gemini
|
|
246
|
+
* rejects the type-less form outright — "Unable to submit request because
|
|
247
|
+
* `t` functionDeclaration `parameters.b` schema specified incorrect schema
|
|
248
|
+
* type field. For schema with properties, schema type should be OBJECT" —
|
|
249
|
+
* on gemini-3.5/3.6/3.7-flash, and rejects a CONTRADICTORY declared type
|
|
250
|
+
* (`{type:"string", properties:{…}}`) the same way, so the type is
|
|
251
|
+
* overwritten rather than merely defaulted. It is not composition-specific:
|
|
252
|
+
* a plain property subschema and an `items` schema fail identically. xAI
|
|
253
|
+
* and OpenAI accept every one of those forms, which is why this survived
|
|
254
|
+
* until a property sweep over recursive shapes went looking for it. The
|
|
255
|
+
* root is exempt in Gemini's own validator, but the rule is applied
|
|
256
|
+
* uniformly because it costs nothing and one rule beats two.
|
|
257
|
+
*
|
|
258
|
+
* It is resolved during the node's type resolution, not patched onto the
|
|
259
|
+
* finished node, so `dropEnum` sees the real type — an `enum` left on a
|
|
260
|
+
* node that has just become an object would otherwise be dropped by the
|
|
261
|
+
* NEXT pass instead of this one, breaking idempotence. A discarded
|
|
262
|
+
* contradictory type is folded into the description like transform #1's.
|
|
263
|
+
* This also makes a contradictory root-union BRANCH repairable: it arrives
|
|
264
|
+
* at transform #9 already retyped, so the constraint survives instead of
|
|
265
|
+
* the whole keyword being dropped.
|
|
266
|
+
*
|
|
267
|
+
* 13. Drop a `pattern` that uses a regex construct a provider's validator
|
|
268
|
+
* refuses, echoing it into the description instead. Measured: `(?=`, `(?!`,
|
|
269
|
+
* `(?<=`, `(?<!` AND named groups `(?<name>` are rejected by gpt-5.6-sol
|
|
270
|
+
* and gpt-5.6-terra (`Invalid JSON schema: regex lookaround is not
|
|
271
|
+
* supported` — the message says lookaround, but a named group fails
|
|
272
|
+
* identically, so the giveaway is the `(?<` prefix); a BACKREFERENCE
|
|
273
|
+
* (`\1`–`\9`) is rejected by grok-4.5. Non-capturing `(?:` is accepted
|
|
274
|
+
* everywhere and is deliberately NOT caught. This is the defect that broke
|
|
275
|
+
* Monad's own `spaces__invite_member` on every OpenAI model — a whole-
|
|
276
|
+
* request 400 from one tool's email pattern. The scan tracks backslash
|
|
277
|
+
* escaping and character classes, so a literal `\(\?=` or a `[(?=]` class
|
|
278
|
+
* keeps its pattern: dropping a pattern that would have been accepted
|
|
279
|
+
* costs a real constraint on every provider.
|
|
280
|
+
* 14. Inline a ROOT union branch that is a local `$ref`, so the union survives
|
|
281
|
+
* rule (a) instead of being dropped whole. `z.union([A, B])` renders as
|
|
282
|
+
* `{anyOf:[{$ref},{$ref}], $defs}` and xAI does not resolve a `$ref` union
|
|
283
|
+
* branch (0/3 even with `type: "object"` on the root) — so rule (a) used to
|
|
284
|
+
* drop the keyword, which for a root whose properties live entirely in
|
|
285
|
+
* `$defs` left the tool advertising NO parameters. Inlining produces
|
|
286
|
+
* ordinary object branches, accepted 9/9. Only branches that are not
|
|
287
|
+
* already usable are inlined, one level deep (a `$ref` inside the target
|
|
288
|
+
* resolves on its own, which is what makes a recursive definition
|
|
289
|
+
* terminate here), with branch keywords winning over the target's per
|
|
290
|
+
* 2020-12 `$ref` semantics and a cap for the same reason the copy-down has
|
|
291
|
+
* one. A branch whose target is not a plain object schema stays unusable
|
|
292
|
+
* and still falls through to the drop.
|
|
103
293
|
*
|
|
104
294
|
* When (1) or (2) discards information the model could use — a collapsed
|
|
105
295
|
* union type, or a wholly-dropped `enum` — that constraint is folded into the
|
|
@@ -146,5 +336,9 @@
|
|
|
146
336
|
* shared by reference, so treat the result as read-only). A non-object root
|
|
147
337
|
* (boolean schema, or malformed third-party payload) yields an empty object
|
|
148
338
|
* rather than throwing.
|
|
339
|
+
*
|
|
340
|
+
* Idempotent: `sanitizeToolSchema(sanitizeToolSchema(x))` deep-equals
|
|
341
|
+
* `sanitizeToolSchema(x)`. Transform #8's copy-down depends on that — a repaired
|
|
342
|
+
* branch that a second pass re-emptied would be no repair at all.
|
|
149
343
|
*/
|
|
150
344
|
export declare function sanitizeToolSchema(schema: Record<string, unknown>): Record<string, unknown>;
|