toolnexus 0.17.0 → 0.18.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/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/src/client.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { Toolkit } from "./toolkit.js"
|
|
7
7
|
import type { ToolResult, Request, Answer } from "./types.js"
|
|
8
8
|
import { pendingOf } from "./types.js"
|
|
9
|
+
import { isRetryableStatus, retryAfterMs } from "./retry.js"
|
|
9
10
|
import type { ContentPart, PromptInput, UnsupportedPartMode } from "./content.js"
|
|
10
11
|
import { toOpenAIWire, toAnthropicWire, checkPromptParts, type WireOptions } from "./wire.js"
|
|
11
12
|
import type { TranslateRequest, TranslateResult, TranslatedToolCall } from "./translate.js"
|
|
@@ -30,10 +31,20 @@ export interface ClientOptions {
|
|
|
30
31
|
systemPrompt?: string
|
|
31
32
|
maxTurns?: number
|
|
32
33
|
hooks?: Hooks
|
|
33
|
-
/** Retries on transient LLM errors (429
|
|
34
|
+
/** Retries on transient LLM errors (`429`/`500`/`502`/`503`/`504`/`529` + network). Default 2.
|
|
35
|
+
* Widen the status set with `retryableStatuses`. */
|
|
34
36
|
retries?: number
|
|
35
37
|
/** Base backoff in ms (exponential + jitter). Default 500. */
|
|
36
38
|
retryBaseMs?: number
|
|
39
|
+
/**
|
|
40
|
+
* Extra HTTP statuses to treat as retryable, ADDED to the default set
|
|
41
|
+
* (`429`/`500`/`502`/`503`/`504`/`529`). It can only widen: a host cannot remove `429` and
|
|
42
|
+
* lose `Retry-After` handling with it. This sets the DEFAULT classification; `onError` still
|
|
43
|
+
* runs per attempt and has the final say, so `onError` returning `"fail"` overrides a status
|
|
44
|
+
* listed here. Example: a Cloudflare-fronted origin that answers `520`–`527`.
|
|
45
|
+
*/
|
|
46
|
+
retryableStatuses?: readonly number[]
|
|
47
|
+
|
|
37
48
|
/** Whole-run deadline in ms; aborts the run (and its in-flight request) when exceeded. */
|
|
38
49
|
timeoutMs?: number
|
|
39
50
|
/** Conversation provider for `ask(prompt, { id })`. Default: in-memory (process lifetime).
|
|
@@ -84,7 +95,8 @@ export interface ErrorInfo {
|
|
|
84
95
|
status?: number
|
|
85
96
|
/** Zero-based attempt index (0 = first try). */
|
|
86
97
|
attempt: number
|
|
87
|
-
/** Whether `status`/the error is in the default retryable set
|
|
98
|
+
/** Whether `status`/the error is in the default retryable set
|
|
99
|
+
* (429/500/502/503/504/529/network, plus anything `retryableStatuses` added). */
|
|
88
100
|
retryable: boolean
|
|
89
101
|
}
|
|
90
102
|
|
|
@@ -97,6 +109,20 @@ export type MetricEvent =
|
|
|
97
109
|
| { event: "llm"; model: string; status: "ok" | "error"; ms: number; promptTokens: number; completionTokens: number }
|
|
98
110
|
| { event: "tool"; tool: string; source: string; isError: boolean; ms: number; pending?: boolean }
|
|
99
111
|
| { event: "run"; model: string; turns: number; toolCalls: number; totalTokens: number; ms: number; error?: string }
|
|
112
|
+
// §8B. The classifier emits into this SAME sink. Neither event is folded into the Prometheus
|
|
113
|
+
// registry, so `client.metrics()` text stays byte-identical to a build with no classifier.
|
|
114
|
+
| {
|
|
115
|
+
event: "classifier.evaluate"
|
|
116
|
+
model: string
|
|
117
|
+
status: "ok" | "error"
|
|
118
|
+
ms: number
|
|
119
|
+
promptTokens: number
|
|
120
|
+
completionTokens: number
|
|
121
|
+
error?: string
|
|
122
|
+
}
|
|
123
|
+
// `warning`, never `error`: a degenerate-criteria report is advisory, and a consumer that
|
|
124
|
+
// counts "has an error" as a failure must not count this.
|
|
125
|
+
| { event: "classifier.warning"; question: string; warning: string }
|
|
100
126
|
|
|
101
127
|
/**
|
|
102
128
|
* Where `ask()` conversations are remembered — two methods. Ship the in-memory
|
|
@@ -121,30 +147,6 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
121
147
|
}
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
const RETRYABLE = new Set([429, 500, 502, 503, 504])
|
|
125
|
-
|
|
126
|
-
/** ~68 years; the widest whole-second count all seven ports represent exactly. */
|
|
127
|
-
const RETRY_AFTER_MAX_SECONDS = 2147483647
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Honour `Retry-After` only in its delay-seconds form: a run of ASCII digits
|
|
131
|
-
* (RFC 9110 §10.2.3) in 0…2147483647, returned in milliseconds.
|
|
132
|
-
*
|
|
133
|
-
* `Number()` is far too permissive for this — it accepts `"0.5"`, `"-5"`, `"1e3"`
|
|
134
|
-
* and `" "` (as 0), which is how this port came to wait for fractional seconds
|
|
135
|
-
* that five of the other six rejected outright. Fractional, signed, HTTP-date and
|
|
136
|
-
* out-of-range values are not delays we can honour, so the caller falls back to
|
|
137
|
-
* backoff rather than guessing. `0` is a real answer, so it returns `0`, not
|
|
138
|
-
* `null`, and callers must use `??` rather than `||`.
|
|
139
|
-
*/
|
|
140
|
-
function retryAfterMs(raw: string | null | undefined): number | null {
|
|
141
|
-
if (raw == null) return null
|
|
142
|
-
const s = raw.trim()
|
|
143
|
-
if (!/^[0-9]+$/.test(s)) return null
|
|
144
|
-
const secs = Number(s)
|
|
145
|
-
return secs <= RETRY_AFTER_MAX_SECONDS ? secs * 1000 : null
|
|
146
|
-
}
|
|
147
|
-
|
|
148
150
|
function delay(ms: number, signal: AbortSignal): Promise<void> {
|
|
149
151
|
return new Promise((resolve, reject) => {
|
|
150
152
|
const t = setTimeout(resolve, ms)
|
|
@@ -665,7 +667,8 @@ export class Client {
|
|
|
665
667
|
return ctrl.signal
|
|
666
668
|
}
|
|
667
669
|
|
|
668
|
-
/** fetch with retry + exponential backoff on
|
|
670
|
+
/** fetch with retry + exponential backoff on the default retryable set (429/500/502/503/504/529)
|
|
671
|
+
* plus network errors and anything `retryableStatuses` added, honoring Retry-After; aborts via signal.
|
|
669
672
|
* The retry-vs-fail decision is the host's `onError` (default: retryable-within-budget ⇒ retry). */
|
|
670
673
|
private async llmFetch(url: string, init: RequestInit, signal: AbortSignal): Promise<Response> {
|
|
671
674
|
const retries = this.opts.retries ?? 2
|
|
@@ -678,7 +681,7 @@ export class Client {
|
|
|
678
681
|
try {
|
|
679
682
|
const res = await (this.opts.fetch ?? fetch)(url, { ...init, signal })
|
|
680
683
|
if (res.ok) return res
|
|
681
|
-
const retryable =
|
|
684
|
+
const retryable = isRetryableStatus(res.status, this.opts.retryableStatuses)
|
|
682
685
|
const tier = classify({ status: res.status, attempt, retryable })
|
|
683
686
|
if (tier === "fail" || attempt === retries) return res // caller surfaces the non-ok status
|
|
684
687
|
const ra = retryAfterMs(res.headers.get("retry-after"))
|
package/src/index.ts
CHANGED
package/src/retry.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
/** ~68 years; the widest whole-second count all seven ports represent exactly. */
|
|
12
|
+
const RETRY_AFTER_MAX_SECONDS = 2147483647
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Honour `Retry-After` only in its delay-seconds form: a run of ASCII digits
|
|
16
|
+
* (RFC 9110 §10.2.3) in 0…2147483647, returned in milliseconds.
|
|
17
|
+
*
|
|
18
|
+
* `Number()` is far too permissive for this — it accepts `"0.5"`, `"-5"`, `"1e3"`
|
|
19
|
+
* and `" "` (as 0), which is how this port came to wait for fractional seconds
|
|
20
|
+
* that five of the other six rejected outright. Fractional, signed, HTTP-date and
|
|
21
|
+
* out-of-range values are not delays we can honour, so the caller falls back to
|
|
22
|
+
* backoff rather than guessing. `0` is a real answer, so it returns `0`, not
|
|
23
|
+
* `null`, and callers must use `??` rather than `||`.
|
|
24
|
+
*/
|
|
25
|
+
export function retryAfterMs(raw: string | null | undefined): number | null {
|
|
26
|
+
if (raw == null) return null
|
|
27
|
+
const s = raw.trim()
|
|
28
|
+
if (!/^[0-9]+$/.test(s)) return null
|
|
29
|
+
const secs = Number(s)
|
|
30
|
+
return secs <= RETRY_AFTER_MAX_SECONDS ? secs * 1000 : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The default retryable set: `429` plus the 5xx worth another try — and `529 Overloaded`,
|
|
35
|
+
* which TypeSafe documents as "retry with backoff" and which an enumeration made terminal.
|
|
36
|
+
*
|
|
37
|
+
* It is an ENUMERATION on purpose. "Any 5xx" would sweep in permanently-broken statuses
|
|
38
|
+
* (`501 Not Implemented`, `505 HTTP Version Not Supported`) and change the retry behaviour of
|
|
39
|
+
* every existing host without asking. A backend with its own transient status — a Cloudflare
|
|
40
|
+
* origin answering `520`–`527`, say — opts in declaratively through `retryableStatuses`, which
|
|
41
|
+
* ADDS to this set and cannot remove from it.
|
|
42
|
+
*
|
|
43
|
+
* Shared with §8B's classifier, which adds `408` to it rather than inventing a second policy.
|
|
44
|
+
*/
|
|
45
|
+
const RETRYABLE = new Set([429, 500, 502, 503, 504, 529])
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a status is retryable by default, optionally widened by a host's `retryableStatuses`.
|
|
49
|
+
*
|
|
50
|
+
* `extra` is ADDITIVE: it can only make more statuses retryable, never fewer, so a host cannot
|
|
51
|
+
* accidentally drop `429` and lose `Retry-After` handling with it. It decides the DEFAULT
|
|
52
|
+
* classification only — `onError` still runs afterwards and has the final say on every attempt,
|
|
53
|
+
* so `onError` returning `"fail"` overrides a status the host itself listed here.
|
|
54
|
+
*/
|
|
55
|
+
export function isRetryableStatus(status: number, extra?: readonly number[] | null): boolean {
|
|
56
|
+
return RETRYABLE.has(status) || (extra != null && extra.includes(status))
|
|
57
|
+
}
|