toolnexus 0.17.0 → 0.18.1
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/README.md +17 -410
- package/dist/agents/loop.js +10 -0
- package/dist/classifier.d.ts +334 -0
- package/dist/classifier.js +630 -0
- package/dist/client.d.ts +26 -3
- package/dist/client.js +4 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/retry.d.ts +30 -0
- package/dist/retry.js +55 -0
- package/package.json +5 -3
- package/src/agents/loop.ts +12 -0
- package/src/classifier.ts +832 -0
- package/src/client.ts +31 -28
- package/src/index.ts +1 -0
- package/src/retry.ts +57 -0
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pendingOf } from "./types.js";
|
|
2
|
+
import { isRetryableStatus, retryAfterMs } from "./retry.js";
|
|
2
3
|
import { toOpenAIWire, toAnthropicWire, checkPromptParts } from "./wire.js";
|
|
3
4
|
import { finishReasonFor, argsString, openAIMessagesToAnthropic, openAIToolsToAnthropic, openAIToolChoiceToAnthropic, hasSystemMessage, } from "./translate.js";
|
|
4
5
|
/** Default conversation provider — keeps transcripts in memory for the client's lifetime. */
|
|
@@ -12,29 +13,6 @@ export class InMemoryConversationStore {
|
|
|
12
13
|
this.map.set(id, [...messages]);
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
|
-
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
|
|
16
|
-
/** ~68 years; the widest whole-second count all seven ports represent exactly. */
|
|
17
|
-
const RETRY_AFTER_MAX_SECONDS = 2147483647;
|
|
18
|
-
/**
|
|
19
|
-
* Honour `Retry-After` only in its delay-seconds form: a run of ASCII digits
|
|
20
|
-
* (RFC 9110 §10.2.3) in 0…2147483647, returned in milliseconds.
|
|
21
|
-
*
|
|
22
|
-
* `Number()` is far too permissive for this — it accepts `"0.5"`, `"-5"`, `"1e3"`
|
|
23
|
-
* and `" "` (as 0), which is how this port came to wait for fractional seconds
|
|
24
|
-
* that five of the other six rejected outright. Fractional, signed, HTTP-date and
|
|
25
|
-
* out-of-range values are not delays we can honour, so the caller falls back to
|
|
26
|
-
* backoff rather than guessing. `0` is a real answer, so it returns `0`, not
|
|
27
|
-
* `null`, and callers must use `??` rather than `||`.
|
|
28
|
-
*/
|
|
29
|
-
function retryAfterMs(raw) {
|
|
30
|
-
if (raw == null)
|
|
31
|
-
return null;
|
|
32
|
-
const s = raw.trim();
|
|
33
|
-
if (!/^[0-9]+$/.test(s))
|
|
34
|
-
return null;
|
|
35
|
-
const secs = Number(s);
|
|
36
|
-
return secs <= RETRY_AFTER_MAX_SECONDS ? secs * 1000 : null;
|
|
37
|
-
}
|
|
38
16
|
function delay(ms, signal) {
|
|
39
17
|
return new Promise((resolve, reject) => {
|
|
40
18
|
const t = setTimeout(resolve, ms);
|
|
@@ -468,7 +446,8 @@ export class Client {
|
|
|
468
446
|
}
|
|
469
447
|
return ctrl.signal;
|
|
470
448
|
}
|
|
471
|
-
/** fetch with retry + exponential backoff on
|
|
449
|
+
/** fetch with retry + exponential backoff on the default retryable set (429/500/502/503/504/529)
|
|
450
|
+
* plus network errors and anything `retryableStatuses` added, honoring Retry-After; aborts via signal.
|
|
472
451
|
* The retry-vs-fail decision is the host's `onError` (default: retryable-within-budget ⇒ retry). */
|
|
473
452
|
async llmFetch(url, init, signal) {
|
|
474
453
|
const retries = this.opts.retries ?? 2;
|
|
@@ -482,7 +461,7 @@ export class Client {
|
|
|
482
461
|
const res = await (this.opts.fetch ?? fetch)(url, { ...init, signal });
|
|
483
462
|
if (res.ok)
|
|
484
463
|
return res;
|
|
485
|
-
const retryable =
|
|
464
|
+
const retryable = isRetryableStatus(res.status, this.opts.retryableStatuses);
|
|
486
465
|
const tier = classify({ status: res.status, attempt, retryable });
|
|
487
466
|
if (tier === "fail" || attempt === retries)
|
|
488
467
|
return res; // caller surfaces the non-ok status
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE §8 retry policy — the retryable-status set and the `Retry-After` rule.
|
|
3
|
+
*
|
|
4
|
+
* INTERNAL to the package. This module is deliberately NOT re-exported from
|
|
5
|
+
* `index.ts`: `client.ts` and `classifier.ts` (§8B) both need the policy, and a
|
|
6
|
+
* second copy is exactly the drift this file exists to prevent — but sharing it
|
|
7
|
+
* is an implementation detail, not npm API. Nothing here is in the parity-checked
|
|
8
|
+
* option surface, and nothing here may be added to `index.ts`.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Honour `Retry-After` only in its delay-seconds form: a run of ASCII digits
|
|
12
|
+
* (RFC 9110 §10.2.3) in 0…2147483647, returned in milliseconds.
|
|
13
|
+
*
|
|
14
|
+
* `Number()` is far too permissive for this — it accepts `"0.5"`, `"-5"`, `"1e3"`
|
|
15
|
+
* and `" "` (as 0), which is how this port came to wait for fractional seconds
|
|
16
|
+
* that five of the other six rejected outright. Fractional, signed, HTTP-date and
|
|
17
|
+
* out-of-range values are not delays we can honour, so the caller falls back to
|
|
18
|
+
* backoff rather than guessing. `0` is a real answer, so it returns `0`, not
|
|
19
|
+
* `null`, and callers must use `??` rather than `||`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function retryAfterMs(raw: string | null | undefined): number | null;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a status is retryable by default, optionally widened by a host's `retryableStatuses`.
|
|
24
|
+
*
|
|
25
|
+
* `extra` is ADDITIVE: it can only make more statuses retryable, never fewer, so a host cannot
|
|
26
|
+
* accidentally drop `429` and lose `Retry-After` handling with it. It decides the DEFAULT
|
|
27
|
+
* classification only — `onError` still runs afterwards and has the final say on every attempt,
|
|
28
|
+
* so `onError` returning `"fail"` overrides a status the host itself listed here.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isRetryableStatus(status: number, extra?: readonly number[] | null): boolean;
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE §8 retry policy — the retryable-status set and the `Retry-After` rule.
|
|
3
|
+
*
|
|
4
|
+
* INTERNAL to the package. This module is deliberately NOT re-exported from
|
|
5
|
+
* `index.ts`: `client.ts` and `classifier.ts` (§8B) both need the policy, and a
|
|
6
|
+
* second copy is exactly the drift this file exists to prevent — but sharing it
|
|
7
|
+
* is an implementation detail, not npm API. Nothing here is in the parity-checked
|
|
8
|
+
* option surface, and nothing here may be added to `index.ts`.
|
|
9
|
+
*/
|
|
10
|
+
/** ~68 years; the widest whole-second count all seven ports represent exactly. */
|
|
11
|
+
const RETRY_AFTER_MAX_SECONDS = 2147483647;
|
|
12
|
+
/**
|
|
13
|
+
* Honour `Retry-After` only in its delay-seconds form: a run of ASCII digits
|
|
14
|
+
* (RFC 9110 §10.2.3) in 0…2147483647, returned in milliseconds.
|
|
15
|
+
*
|
|
16
|
+
* `Number()` is far too permissive for this — it accepts `"0.5"`, `"-5"`, `"1e3"`
|
|
17
|
+
* and `" "` (as 0), which is how this port came to wait for fractional seconds
|
|
18
|
+
* that five of the other six rejected outright. Fractional, signed, HTTP-date and
|
|
19
|
+
* out-of-range values are not delays we can honour, so the caller falls back to
|
|
20
|
+
* backoff rather than guessing. `0` is a real answer, so it returns `0`, not
|
|
21
|
+
* `null`, and callers must use `??` rather than `||`.
|
|
22
|
+
*/
|
|
23
|
+
export function retryAfterMs(raw) {
|
|
24
|
+
if (raw == null)
|
|
25
|
+
return null;
|
|
26
|
+
const s = raw.trim();
|
|
27
|
+
if (!/^[0-9]+$/.test(s))
|
|
28
|
+
return null;
|
|
29
|
+
const secs = Number(s);
|
|
30
|
+
return secs <= RETRY_AFTER_MAX_SECONDS ? secs * 1000 : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The default retryable set: `429` plus the 5xx worth another try — and `529 Overloaded`,
|
|
34
|
+
* which TypeSafe documents as "retry with backoff" and which an enumeration made terminal.
|
|
35
|
+
*
|
|
36
|
+
* It is an ENUMERATION on purpose. "Any 5xx" would sweep in permanently-broken statuses
|
|
37
|
+
* (`501 Not Implemented`, `505 HTTP Version Not Supported`) and change the retry behaviour of
|
|
38
|
+
* every existing host without asking. A backend with its own transient status — a Cloudflare
|
|
39
|
+
* origin answering `520`–`527`, say — opts in declaratively through `retryableStatuses`, which
|
|
40
|
+
* ADDS to this set and cannot remove from it.
|
|
41
|
+
*
|
|
42
|
+
* Shared with §8B's classifier, which adds `408` to it rather than inventing a second policy.
|
|
43
|
+
*/
|
|
44
|
+
const RETRYABLE = new Set([429, 500, 502, 503, 504, 529]);
|
|
45
|
+
/**
|
|
46
|
+
* Whether a status is retryable by default, optionally widened by a host's `retryableStatuses`.
|
|
47
|
+
*
|
|
48
|
+
* `extra` is ADDITIVE: it can only make more statuses retryable, never fewer, so a host cannot
|
|
49
|
+
* accidentally drop `429` and lose `Retry-After` handling with it. It decides the DEFAULT
|
|
50
|
+
* classification only — `onError` still runs afterwards and has the final say on every attempt,
|
|
51
|
+
* so `onError` returning `"fail"` overrides a status the host itself listed here.
|
|
52
|
+
*/
|
|
53
|
+
export function isRetryableStatus(status, extra) {
|
|
54
|
+
return RETRYABLE.has(status) || (extra != null && extra.includes(status));
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolnexus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Dynamic MCP servers + agent skills as uniform tools for any LLM (opencode-style).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"test": "npm run build && node --experimental-strip-types --test test/unit.test.ts test/agents.test.ts test/agent-hooks.test.ts test/home.test.ts test/compaction.test.ts test/loop.test.ts test/in-process.test.ts test/translate.test.ts test/multimodal.test.ts",
|
|
21
|
-
"example": "node --experimental-strip-types examples/basic.ts"
|
|
20
|
+
"test": "npm run build && node --experimental-strip-types --test test/unit.test.ts test/agents.test.ts test/agent-hooks.test.ts test/home.test.ts test/compaction.test.ts test/loop.test.ts test/in-process.test.ts test/translate.test.ts test/multimodal.test.ts test/classifier.test.ts",
|
|
21
|
+
"example": "node --experimental-strip-types examples/basic.ts",
|
|
22
|
+
"example:judge": "node --experimental-strip-types examples/judge.ts",
|
|
23
|
+
"example:multimodal": "node --experimental-strip-types examples/multimodal.ts"
|
|
22
24
|
},
|
|
23
25
|
"keywords": [
|
|
24
26
|
"mcp",
|
package/src/agents/loop.ts
CHANGED
|
@@ -72,6 +72,18 @@ export function guardedHooks(guardrails: Guardrail[] | undefined, hooks: Hooks |
|
|
|
72
72
|
beforeTool: async (ev) => {
|
|
73
73
|
for (const g of guardrails) {
|
|
74
74
|
const verdict = g(ev)
|
|
75
|
+
// A guardrail is SYNCHRONOUS by contract. An async one returns a Promise,
|
|
76
|
+
// which is truthy and !== "allow", so it would silently DENY every call
|
|
77
|
+
// with `denied: [object Promise]`. Fail loudly instead, and say where
|
|
78
|
+
// asynchronous work belongs.
|
|
79
|
+
if (verdict != null && typeof verdict !== "string") {
|
|
80
|
+
const what = typeof (verdict as { then?: unknown }).then === "function" ? "a Promise" : typeof verdict
|
|
81
|
+
throw new TypeError(
|
|
82
|
+
`guardrail returned ${what}: a guardrail must return a string synchronously ` +
|
|
83
|
+
`("" or "allow" to permit, any other string to deny). ` +
|
|
84
|
+
`Do asynchronous work in hooks.beforeTool, which is awaited.`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
75
87
|
if (verdict && verdict !== "allow") {
|
|
76
88
|
return { result: { output: `denied: ${verdict}`, isError: true } }
|
|
77
89
|
}
|