@juno-ai/bind 2.0.0 → 4.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.
Files changed (52) hide show
  1. package/README.md +1153 -60
  2. package/contracts/index.d.ts +1 -1
  3. package/contracts/index.js +1 -1
  4. package/contracts/turn.d.ts +31 -7
  5. package/contracts/turn.js +45 -0
  6. package/index.d.ts +16 -5
  7. package/index.js +16 -5
  8. package/loop/index.d.ts +1 -0
  9. package/loop/index.js +1 -0
  10. package/loop/tool-loop.d.ts +260 -0
  11. package/loop/tool-loop.js +276 -0
  12. package/package.json +22 -2
  13. package/plugins/activation.d.ts +67 -0
  14. package/plugins/activation.js +61 -0
  15. package/plugins/index.d.ts +3 -0
  16. package/plugins/index.js +3 -0
  17. package/plugins/registry.d.ts +52 -0
  18. package/plugins/registry.js +54 -0
  19. package/plugins/tool.d.ts +164 -0
  20. package/plugins/tool.js +9 -0
  21. package/routing/billing-basis.d.ts +48 -0
  22. package/routing/billing-basis.js +67 -0
  23. package/routing/circuit-breaker.d.ts +2 -2
  24. package/routing/errors.d.ts +1 -1
  25. package/routing/executor.d.ts +3 -3
  26. package/routing/executor.js +1 -1
  27. package/routing/index.d.ts +11 -9
  28. package/routing/index.js +11 -9
  29. package/routing/plan-degradation.d.ts +34 -0
  30. package/routing/plan-degradation.js +38 -0
  31. package/routing/plan.d.ts +2 -2
  32. package/routing/planner.d.ts +4 -4
  33. package/routing/planner.js +1 -1
  34. package/routing/policy.d.ts +1 -1
  35. package/routing/policy.js +1 -1
  36. package/routing/transport.d.ts +2 -2
  37. package/run/children.d.ts +204 -0
  38. package/run/children.js +226 -0
  39. package/run/harness.d.ts +94 -0
  40. package/run/harness.js +140 -0
  41. package/run/index.d.ts +3 -0
  42. package/run/index.js +3 -0
  43. package/run/tool-batch.d.ts +16 -0
  44. package/run/tool-batch.js +83 -0
  45. package/tools/index.d.ts +1 -0
  46. package/tools/index.js +1 -0
  47. package/tools/sanitize-schema.d.ts +150 -0
  48. package/tools/sanitize-schema.js +683 -0
  49. package/transcript/index.d.ts +1 -0
  50. package/transcript/index.js +1 -0
  51. package/transcript/validate.d.ts +54 -0
  52. package/transcript/validate.js +226 -0
@@ -0,0 +1,16 @@
1
+ import type OpenAI from "openai";
2
+ /**
3
+ * Run a batch of tool calls concurrently with per-tool-name pooling. Calls
4
+ * to different tools fan out fully; calls to the *same* tool are capped at
5
+ * `PER_TOOL_POOL_SIZE` so a batch of many calls to one downstream service does
6
+ * not overwhelm it. Results are
7
+ * returned in the same order as `calls`, each as a `PromiseSettledResult`
8
+ * so the caller can synthesize error tool messages for any rejections
9
+ * (every `tool_call_id` in the assistant message still needs a response).
10
+ *
11
+ * Generic over the per-call outcome: the pooling and ordering policy is
12
+ * harness-owned, while the shape a host derives from a completed call (which
13
+ * plugin activated, whether the run should compact, a suspend directive, …)
14
+ * stays with the host.
15
+ */
16
+ export declare function runToolCallsPooledByTool<TOutcome>(calls: OpenAI.ChatCompletionMessageToolCall[], run: (tc: OpenAI.ChatCompletionMessageToolCall) => Promise<TOutcome>): Promise<PromiseSettledResult<TOutcome>[]>;
@@ -0,0 +1,83 @@
1
+ // Per-tool concurrency ceiling inside a single LLM tool-call batch. A model
2
+ // response can request many calls to the same tool; running them all at once
3
+ // would hammer whatever it talks to (a search backend, a database, a
4
+ // third-party API). Calls to *different* tools still fan out fully — the cap
5
+ // is applied per tool name, not globally.
6
+ const PER_TOOL_POOL_SIZE = 5;
7
+ /**
8
+ * The name a batch pools on. `function` and `custom` tool calls both carry a
9
+ * name, on different fields; anything the wire union grows later falls back to
10
+ * its type, which pools all such calls together rather than guessing.
11
+ */
12
+ function poolKey(call) {
13
+ switch (call.type) {
14
+ case "function":
15
+ return call.function.name;
16
+ case "custom":
17
+ return `__custom__:${call.custom.name}`;
18
+ default: {
19
+ // Not `never`: this union is a third party's, and a new member must not
20
+ // become a type error in a consumer that never sees one.
21
+ const unknownCall = call;
22
+ return `__${unknownCall.type ?? "unknown"}__`;
23
+ }
24
+ }
25
+ }
26
+ /**
27
+ * Run a batch of tool calls concurrently with per-tool-name pooling. Calls
28
+ * to different tools fan out fully; calls to the *same* tool are capped at
29
+ * `PER_TOOL_POOL_SIZE` so a batch of many calls to one downstream service does
30
+ * not overwhelm it. Results are
31
+ * returned in the same order as `calls`, each as a `PromiseSettledResult`
32
+ * so the caller can synthesize error tool messages for any rejections
33
+ * (every `tool_call_id` in the assistant message still needs a response).
34
+ *
35
+ * Generic over the per-call outcome: the pooling and ordering policy is
36
+ * harness-owned, while the shape a host derives from a completed call (which
37
+ * plugin activated, whether the run should compact, a suspend directive, …)
38
+ * stays with the host.
39
+ */
40
+ export async function runToolCallsPooledByTool(calls, run) {
41
+ if (calls.length === 0)
42
+ return [];
43
+ const results = new Array(calls.length);
44
+ const groups = new Map();
45
+ for (let i = 0; i < calls.length; i++) {
46
+ const tc = calls[i];
47
+ // Pool key is the tool name, wherever the wire union puts it — `custom`
48
+ // tool calls carry theirs on a different field, and pooling every one of
49
+ // them together would serialize unrelated tools behind each other. A future
50
+ // variant with no name at all falls back to its type; the `__` fencing
51
+ // keeps either synthetic key from colliding with a real tool name.
52
+ const key = poolKey(tc);
53
+ let arr = groups.get(key);
54
+ if (!arr) {
55
+ arr = [];
56
+ groups.set(key, arr);
57
+ }
58
+ arr.push(i);
59
+ }
60
+ await Promise.all(Array.from(groups.values()).map(async (indices) => {
61
+ // `cursor++` in single-threaded JS is atomic between awaits — no lock
62
+ // needed. Workers race for the next index; when cursor exceeds the
63
+ // group size the worker returns.
64
+ let cursor = 0;
65
+ const workerCount = Math.min(PER_TOOL_POOL_SIZE, indices.length);
66
+ await Promise.all(Array.from({ length: workerCount }, async () => {
67
+ while (true) {
68
+ const pos = cursor++;
69
+ if (pos >= indices.length)
70
+ return;
71
+ const callIdx = indices[pos];
72
+ try {
73
+ const value = await run(calls[callIdx]);
74
+ results[callIdx] = { status: "fulfilled", value };
75
+ }
76
+ catch (reason) {
77
+ results[callIdx] = { status: "rejected", reason };
78
+ }
79
+ }
80
+ }));
81
+ }));
82
+ return results;
83
+ }
@@ -0,0 +1 @@
1
+ export { sanitizeToolSchema } from "./sanitize-schema.js";
package/tools/index.js ADDED
@@ -0,0 +1 @@
1
+ export { sanitizeToolSchema } from "./sanitize-schema.js";
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Sanitize a tool's JSON Schema before it is handed to the model.
3
+ *
4
+ * Why this exists
5
+ * ---------------
6
+ * Tool schemas reach the LLM verbatim — for MCP tools the third-party
7
+ * server's `inputSchema` is forwarded as-is (`rawJsonSchema` in
8
+ * `buildToolDefinitions`). Some providers validate that schema strictly
9
+ * and reject the *entire* request (every tool, not just the bad one) on
10
+ * the first violation. The one that bit us in production was Google
11
+ * Gemini via OpenRouter:
12
+ *
13
+ * OpenRouter 400 — Provider: Google (Google AI Studio), INVALID_ARGUMENT:
14
+ * GenerateContentRequest.tools[0].function_declarations[50]
15
+ * .parameters.properties[issue_fields].items.required[0]:
16
+ * property is not defined
17
+ *
18
+ * That message is a *symptom*, not the cause. The GitHub MCP `issue_write`
19
+ * tool's `issue_fields.items` schema contained constructs Gemini's
20
+ * function-declaration schema (a strict OpenAPI 3.0 subset) does not
21
+ * support: a property typed with a `type` ARRAY (`value: { type:
22
+ * ["string","number","boolean"] }`) and an `enum` on a boolean property
23
+ * (`delete: { type: "boolean", enum: [true] }`). When Gemini's translator
24
+ * hits an unsupported construct it drops the enclosing `properties` object,
25
+ * after which the (perfectly valid) `required: ["field_name"]` dangles —
26
+ * hence the misleading "property is not defined". The root causes were
27
+ * confirmed empirically by bisecting the real schema against live Gemini
28
+ * inference via OpenRouter; OpenAI and Anthropic accept all of it.
29
+ *
30
+ * What it does (applied to EVERY tool for EVERY model)
31
+ * ----------------------------------------------------
32
+ * The transforms only remove or normalize constructs that carry no real
33
+ * constraint for a model's tool call. Each was verified against live Gemini:
34
+ *
35
+ * 1. Collapse a `type` ARRAY to a single `type`. Gemini requires a scalar
36
+ * `type` and rejects a union array. We keep the first non-`"null"`
37
+ * member (e.g. `["string","number","boolean"]` → `"string"`,
38
+ * `["string","null"]` → `"string"`); an array of only `"null"` drops
39
+ * the `type`. The lost alternatives are advisory — arguments are still
40
+ * validated at dispatch against the tool's zod schema and by the remote
41
+ * MCP server.
42
+ * 2. Constrain `enum` to Gemini's rule: it is accepted ONLY as a list of
43
+ * strings on a string-typed (or type-less) property. So drop `enum`
44
+ * entirely when the node has an explicit non-string type (boolean,
45
+ * number, integer, array, object, null), and otherwise filter it to its
46
+ * string members (dropping any non-string leftovers — e.g. after a union
47
+ * `type` collapsed to "string") and omit it if none remain. `const`,
48
+ * which Gemini DOES support on every type, is left untouched — do not
49
+ * convert it to `enum`, which would manufacture the rejected case.
50
+ * 3. Drop `required` entries with no matching key in the SAME node's own
51
+ * `properties` map (evaluated per node, against direct `properties`
52
+ * only — Gemini does not resolve composition or parent scope, and a
53
+ * `required` naming an absent property is a hard 400). A genuinely
54
+ * dangling `required` is invalid everywhere; this is a safety net, not
55
+ * the primary fix for the issue above.
56
+ * 4. Strip the `$schema` dialect declaration, which function-calling APIs
57
+ * ignore. We deliberately KEEP `$id`: for schemas that use relative
58
+ * `$ref`s it defines the base URI / identifies subschemas, and we
59
+ * preserve `$ref`, so dropping `$id` could break reference resolution.
60
+ * 5. Flatten a top-level `allOf` of object subschemas into a single object
61
+ * schema (merge `properties`, union `required`). A function's parameters
62
+ * root produced by a Zod intersection (`a.and(b)`) renders as `{ allOf:
63
+ * [ {type:object…}, {type:object…} ] }` with no root `type`/`properties`.
64
+ * xAI (Grok) via OpenRouter strictly requires the parameters ROOT to be a
65
+ * plain object schema and rejects the whole request with a 502 "Invalid
66
+ * arguments passed to the model" on a composition root; Gemini tolerates
67
+ * it. Merging is safe for tool calling — `allOf` means "satisfy every
68
+ * branch", i.e. an object carrying the union of all branches' properties.
69
+ * Confirmed empirically against live Grok (`document__convert_excel` /
70
+ * `document__ocr`). Only `allOf` is flattened (a true intersection);
71
+ * `anyOf`/`oneOf` roots are left untouched. The flatten is applied ONLY
72
+ * when it's lossless — every branch is a plain object schema with
73
+ * merge-safe keywords; a branch carrying `$ref`, `not`, `if`, nested
74
+ * composition, a non-object `type`, etc. leaves the `allOf` intact rather
75
+ * than dropping the unmerged constraint.
76
+ * 6. Drop a boolean `additionalProperties: false`. xAI (Grok) via OpenRouter
77
+ * rejects that form on a NESTED object schema with a hard 400 ("property
78
+ * schema 'false' is not supported"), failing the WHOLE request (every
79
+ * tool). The constraint it carries — "no keys beyond those declared" — is
80
+ * advisory for tool calling (arguments are validated at dispatch against
81
+ * the tool's zod schema and by the remote MCP server), so it is dropped
82
+ * rather than allowed to sink the request. `additionalProperties: true`
83
+ * (the JSON Schema default, a no-op) is left as-is, and an OBJECT-valued
84
+ * `additionalProperties` is a real subschema constraint every provider
85
+ * accepts — it is kept and recursed into. Confirmed against the production
86
+ * failure (Notion `notion-create-pages`,
87
+ * `pages[].properties.properties.additionalProperties`).
88
+ * 7. Neutralize a tool parameter literally NAMED `properties` whose schema is
89
+ * object-shaped. xAI (Grok) via OpenRouter mis-reads such a field as the
90
+ * JSON-Schema `properties` keyword — it descends in, injects an
91
+ * `additionalProperties: false`, then rejects its own injection, failing the
92
+ * WHOLE request with `/properties/properties/additionalProperties: property
93
+ * schema 'false' is not supported` (so transform #6 alone never helped —
94
+ * the rejected `false` is xAI's own, not ours). Notion's `notion-create-pages`
95
+ * / `update-page` carry exactly this field (the page's `properties` map), so
96
+ * it 400s the default Grok agent. xAI accepts the field only when it does NOT
97
+ * look like an object schema, so we collapse an object-shaped
98
+ * `properties`-named node to annotations only (`description` / `title`).
99
+ * Advisory-safe (args validated at dispatch + by the remote server; Notion's
100
+ * `properties` is an open per-database map with no fixed sub-schema), and the
101
+ * key name is never changed so the model still emits the right argument key.
102
+ * Confirmed live: `x-ai/grok-4.3` 400→200, Gemini/Claude unaffected.
103
+ *
104
+ * When (1) or (2) discards information the model could use — a collapsed
105
+ * union type, or a wholly-dropped `enum` — that constraint is folded into the
106
+ * node's `description` as prose ("Accepts string, number, or boolean.",
107
+ * "Allowed values: true.") so the model still sees it. `description` is a free
108
+ * string every provider accepts, so this is always safe.
109
+ *
110
+ * It deliberately leaves meaningful validation keywords (`format`,
111
+ * `pattern`, object- or `true`-valued `additionalProperties`, `const`, string
112
+ * `enum`, length/range bounds, `$ref`, `$id`) intact so compliant providers
113
+ * keep their guidance. The two structural exceptions are both xAI-rejected: the
114
+ * boolean `false` form of `additionalProperties` (transform #6), and the
115
+ * object-structure of a property literally named `properties` (transform #7),
116
+ * which is collapsed to annotations only.
117
+ *
118
+ * The walk is keyword-aware: it only recurses into positions that JSON
119
+ * Schema defines as subschemas, and treats `enum` / `const` / `default` /
120
+ * `examples` values (and the `required` array itself) as opaque data. That
121
+ * way a tool parameter literally named `properties` or `required`, or an
122
+ * example value that happens to contain those keys, is never mistaken for
123
+ * schema structure and corrupted by OUR walk. (Transform #7 separately
124
+ * rewrites a `properties`-named object field — not because we confuse it, but
125
+ * because xAI's parser does.)
126
+ *
127
+ * Robustness: the walk is depth-bounded (`MAX_DEPTH`) so a pathologically
128
+ * nested third-party schema can't overflow the stack, and the entry point
129
+ * tolerates a non-object root (a boolean schema, or a malformed server's
130
+ * payload) by returning an empty object instead of throwing. Past the bound
131
+ * the subtree is *replaced*, not returned verbatim — a bomb passed through
132
+ * would simply overflow one frame later, when the caller serializes the
133
+ * result into a request body. For the same reason nothing in this module
134
+ * hands an untrusted value to `JSON.stringify`, which recurses on its own and
135
+ * so would reintroduce the unbounded walk the depth guard exists to prevent.
136
+ *
137
+ * Non-mutating, but NOT a full deep clone: the returned object has a fresh
138
+ * spine (every object/array node the walk descends into is rebuilt), so the
139
+ * input `rawJsonSchema` — shared across runs — is never mutated. Opaque
140
+ * leaf values (`enum`, `const`, `default`, `examples`, `pattern`, …) are
141
+ * carried over by reference, so callers must treat the result as read-only.
142
+ */
143
+ /**
144
+ * Return a sanitized, non-mutating copy of a tool parameter JSON Schema (see
145
+ * the module header for the immutability caveat — opaque leaf values are
146
+ * shared by reference, so treat the result as read-only). A non-object root
147
+ * (boolean schema, or malformed third-party payload) yields an empty object
148
+ * rather than throwing.
149
+ */
150
+ export declare function sanitizeToolSchema(schema: Record<string, unknown>): Record<string, unknown>;