@fungi.computer/mule 0.2.0 → 0.2.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/dist/effect.d.ts +1 -1
- package/dist/effect.js +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/provider-status.d.ts +12 -2
- package/dist/provider-status.js +117 -14
- package/docs/api/effect.md +14 -0
- package/docs/api/index.md +30 -1
- package/package.json +1 -1
package/dist/effect.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { decodeProviderData } from "./provider-data.js";
|
|
2
2
|
export { streamPi as streamModel } from "./provider.js";
|
|
3
|
-
export { observeProviderStatus, type ProviderRejection, type ProviderStatusRecorder, } from "./provider-status.js";
|
|
3
|
+
export { isRetryableProviderRejection, observeProviderStatus, type ProviderRejection, type ProviderStatusRecorder, } from "./provider-status.js";
|
|
4
4
|
export { normalizeModel } from "./provider-model.js";
|
|
5
5
|
import type { AssistantMessage, Message, RetryPolicy, Tool, ToolCall } from "./types.js";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
package/dist/effect.js
CHANGED
|
@@ -2,7 +2,7 @@ import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
|
2
2
|
export { decodeProviderData } from "./provider-data.js";
|
|
3
3
|
export { streamPi as streamModel } from "./provider.js";
|
|
4
4
|
import { nanoid } from "@fungi.computer/cairn";
|
|
5
|
-
export { observeProviderStatus, } from "./provider-status.js";
|
|
5
|
+
export { isRetryableProviderRejection, observeProviderStatus, } from "./provider-status.js";
|
|
6
6
|
// Pi's root entry also evaluates its TypeBox validator compiler and model
|
|
7
7
|
// stores; Mule needs only these leaf helpers, so it imports them directly and
|
|
8
8
|
// keeps that work out of every host's startup.
|
|
@@ -547,12 +547,16 @@ function streamTurnWithRetry(input, context, run, turn, emit) {
|
|
|
547
547
|
try: () => input.retryAfterMs?.(message),
|
|
548
548
|
catch: (cause) => failure("model", "Model retry delay observation failed", cause),
|
|
549
549
|
});
|
|
550
|
-
const
|
|
550
|
+
const minimumDelayMs = Math.max(retryDelayMs(policy, retries), after !== undefined &&
|
|
551
551
|
Number.isSafeInteger(after) &&
|
|
552
552
|
after >= 0 &&
|
|
553
553
|
after <= 60_000
|
|
554
554
|
? after
|
|
555
555
|
: 0);
|
|
556
|
+
// Add rather than subtract jitter: a provider's Retry-After is a floor.
|
|
557
|
+
// Jitter stops at one minute; a longer explicit host policy stays intact.
|
|
558
|
+
const jitterMs = Math.min(1_000, minimumDelayMs / 4, Math.max(0, 60_000 - minimumDelayMs));
|
|
559
|
+
const delayMs = minimumDelayMs + Math.floor(Math.random() * jitterMs);
|
|
556
560
|
yield* reportRetry({
|
|
557
561
|
type: "retry_scheduled",
|
|
558
562
|
run,
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { decodeProviderData } from "./provider-data.js";
|
|
|
2
2
|
export { streamPi as streamModel } from "./provider.js";
|
|
3
3
|
export { normalizeModel } from "./provider-model.js";
|
|
4
4
|
import type { Message, Tool, ToolCall } from "./types.js";
|
|
5
|
-
export { observeProviderStatus, type ProviderRejection, type ProviderStatusRecorder, } from "./provider-status.js";
|
|
5
|
+
export { isRetryableProviderRejection, observeProviderStatus, type ProviderRejection, type ProviderStatusRecorder, } from "./provider-status.js";
|
|
6
6
|
import type { MuleCompletedTurn, MuleEvent, MuleRunOutcome, MuleRunSettings, MuleToolExecutionPart, MuleToolRejection } from "./contract.js";
|
|
7
7
|
export type { MuleCompletedTurn, MuleResume, MuleToolCheckpoint, MuleError, MuleEvent, MuleIdentity, MuleIdentityKind, MuleRunOutcome, MuleRunSettings, MuleStreamFunction, MuleToolExecutionPart, MuleToolRejection, MuleToolResult, } from "./contract.js";
|
|
8
8
|
/** Promise listener adapted once into Mule's canonical Effect program. */
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { decodeProviderData } from "./provider-data.js";
|
|
2
2
|
export { streamPi as streamModel } from "./provider.js";
|
|
3
3
|
export { normalizeModel } from "./provider-model.js";
|
|
4
|
-
export { observeProviderStatus, } from "./provider-status.js";
|
|
4
|
+
export { isRetryableProviderRejection, observeProviderStatus, } from "./provider-status.js";
|
|
5
5
|
import * as Cause from "effect/Cause";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as Exit from "effect/Exit";
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import type { SimpleStreamOptions } from "./types.js";
|
|
1
|
+
import type { AssistantMessage, SimpleStreamOptions } from "./types.js";
|
|
2
2
|
/** Bounded provider evidence, never response bodies, headers or error prose. */
|
|
3
3
|
export type ProviderRejection = Readonly<{
|
|
4
4
|
code: "usage-limit-reached";
|
|
5
|
+
/** Provider-declared epoch milliseconds, absent when unknown. */
|
|
6
|
+
resetAt?: number;
|
|
7
|
+
limit?: "five_hour" | "seven_day" | "seven_day_opus" | "seven_day_sonnet";
|
|
8
|
+
}> | Readonly<{
|
|
9
|
+
code: "rate-limit";
|
|
10
|
+
retryAfterMs?: number;
|
|
11
|
+
}> | Readonly<{
|
|
12
|
+
code: "overloaded";
|
|
5
13
|
}> | Readonly<{
|
|
6
14
|
code: "provider_unavailable";
|
|
7
15
|
status?: number;
|
|
@@ -9,6 +17,8 @@ export type ProviderRejection = Readonly<{
|
|
|
9
17
|
code: "provider_rejected";
|
|
10
18
|
status: number;
|
|
11
19
|
}>;
|
|
20
|
+
/** Retry only typed transient failures, never usage exhaustion or rejection. */
|
|
21
|
+
export declare function isRetryableProviderRejection(rejection: ProviderRejection | undefined): boolean;
|
|
12
22
|
/** One provider call's transport and parsed-event observations. */
|
|
13
23
|
export type ProviderStatusRecorder = Readonly<{
|
|
14
24
|
/** Request-local fetch observer, retaining the supplied transport. */
|
|
@@ -16,7 +26,7 @@ export type ProviderStatusRecorder = Readonly<{
|
|
|
16
26
|
/** Pi's maintained parsed-event hook, composed with the supplied observer. */
|
|
17
27
|
onProviderStreamEvent: NonNullable<SimpleStreamOptions["onProviderStreamEvent"]>;
|
|
18
28
|
/** Latest typed failure evidence for this call, or no observed failure. */
|
|
19
|
-
rejection: () => ProviderRejection | undefined;
|
|
29
|
+
rejection: (message?: AssistantMessage) => ProviderRejection | undefined;
|
|
20
30
|
}>;
|
|
21
31
|
/**
|
|
22
32
|
* Observe one call's typed HTTP and parsed-event evidence. Pi owns stream
|
package/dist/provider-status.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Option, Schema } from "effect";
|
|
1
|
+
import { DateTime, Option, Schema } from "effect";
|
|
2
2
|
const usageLimitCode = "subscription_sharing_usage_limit_exceeded";
|
|
3
3
|
const providerError = Schema.Struct({
|
|
4
4
|
error: Schema.Struct({
|
|
5
|
-
code: Schema.String,
|
|
5
|
+
code: Schema.optional(Schema.String),
|
|
6
|
+
type: Schema.optional(Schema.String),
|
|
6
7
|
}),
|
|
7
8
|
});
|
|
8
9
|
const decodeFailedEvent = Schema.decodeUnknownOption(Schema.Struct({
|
|
@@ -12,13 +13,70 @@ const decodeFailedEvent = Schema.decodeUnknownOption(Schema.Struct({
|
|
|
12
13
|
const decodeErrorReply = Schema.decodeUnknownOption(Schema.fromJsonString(providerError));
|
|
13
14
|
const decodeBodyChunk = Schema.decodeUnknownSync(Schema.instanceOf(Uint8Array));
|
|
14
15
|
const errorBodyBytes = 16 * 1024;
|
|
16
|
+
const decodeSeconds = Schema.decodeUnknownOption(Schema.NumberFromString.check(Schema.isBetween({ minimum: 0, maximum: 60 })));
|
|
17
|
+
const decodeReset = Schema.decodeUnknownOption(Schema.NumberFromString.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 8_640_000_000_000 })));
|
|
18
|
+
const decodeLimit = Schema.decodeUnknownOption(Schema.Literals([
|
|
19
|
+
"five_hour",
|
|
20
|
+
"seven_day",
|
|
21
|
+
"seven_day_opus",
|
|
22
|
+
"seven_day_sonnet",
|
|
23
|
+
]));
|
|
24
|
+
const decodeRejected = Schema.decodeUnknownOption(Schema.Literal("rejected"));
|
|
25
|
+
const decodeExhausted = Schema.decodeUnknownOption(Schema.Literal("0"));
|
|
26
|
+
const decodeDate = Schema.decodeUnknownOption(Schema.DateTimeUtcFromString);
|
|
27
|
+
const decodeOverloaded = Schema.decodeUnknownOption(Schema.Struct({
|
|
28
|
+
type: Schema.Literal("error"),
|
|
29
|
+
error: Schema.Struct({ type: Schema.Literal("overloaded_error") }),
|
|
30
|
+
}));
|
|
31
|
+
function resetDelay(value, now) {
|
|
32
|
+
const date = decodeDate(value);
|
|
33
|
+
if (Option.isNone(date))
|
|
34
|
+
return undefined;
|
|
35
|
+
const delay = DateTime.toEpochMillis(date.value) - now;
|
|
36
|
+
return delay >= 0 && delay <= 60_000 ? Math.ceil(delay) : undefined;
|
|
37
|
+
}
|
|
38
|
+
function rateLimit(headers) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const header = headers.get("retry-after");
|
|
41
|
+
const seconds = decodeSeconds(header);
|
|
42
|
+
let delay = Option.isSome(seconds)
|
|
43
|
+
? Math.ceil(seconds.value * 1000)
|
|
44
|
+
: resetDelay(header, now);
|
|
45
|
+
// API-key rate windows are not subscription caps. Only an explicitly
|
|
46
|
+
// exhausted dimension supplies a retry floor; missing facts stay unknown.
|
|
47
|
+
for (const dimension of [
|
|
48
|
+
"requests",
|
|
49
|
+
"tokens",
|
|
50
|
+
"input-tokens",
|
|
51
|
+
"output-tokens",
|
|
52
|
+
]) {
|
|
53
|
+
if (Option.isNone(decodeExhausted(headers.get(`anthropic-ratelimit-${dimension}-remaining`))))
|
|
54
|
+
continue;
|
|
55
|
+
const reset = resetDelay(headers.get(`anthropic-ratelimit-${dimension}-reset`), now);
|
|
56
|
+
if (reset !== undefined)
|
|
57
|
+
delay = Math.max(delay ?? 0, reset);
|
|
58
|
+
}
|
|
59
|
+
return delay !== undefined
|
|
60
|
+
? { code: "rate-limit", retryAfterMs: delay }
|
|
61
|
+
: { code: "rate-limit" };
|
|
62
|
+
}
|
|
63
|
+
function usageLimit(headers) {
|
|
64
|
+
const reset = decodeReset(headers.get("anthropic-ratelimit-unified-reset"));
|
|
65
|
+
const limit = decodeLimit(headers.get("anthropic-ratelimit-unified-representative-claim"));
|
|
66
|
+
let result = { code: "usage-limit-reached" };
|
|
67
|
+
if (Option.isSome(reset))
|
|
68
|
+
result = { ...result, resetAt: reset.value * 1000 };
|
|
69
|
+
if (Option.isSome(limit))
|
|
70
|
+
result = { ...result, limit: limit.value };
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
15
73
|
/** Pi's pinned provider-request transient classes, kept as typed evidence. */
|
|
16
74
|
const transientStatus = (status) => status === 408 ||
|
|
17
75
|
status === 409 ||
|
|
18
76
|
status === 429 ||
|
|
19
77
|
(status >= 500 && status <= 599);
|
|
20
78
|
/** Retain Local's bounded error-copy observation; Pi still owns its response. */
|
|
21
|
-
async function
|
|
79
|
+
async function providerErrorKind(response) {
|
|
22
80
|
const body = response.clone().body;
|
|
23
81
|
if (body === null)
|
|
24
82
|
return undefined;
|
|
@@ -39,7 +97,9 @@ async function providerErrorCode(response) {
|
|
|
39
97
|
}
|
|
40
98
|
text += decoder.decode();
|
|
41
99
|
const decoded = decodeErrorReply(text);
|
|
42
|
-
return Option.isSome(decoded)
|
|
100
|
+
return Option.isSome(decoded)
|
|
101
|
+
? (decoded.value.error.code ?? decoded.value.error.type)
|
|
102
|
+
: undefined;
|
|
43
103
|
}
|
|
44
104
|
finally {
|
|
45
105
|
// A tee's cancellation may await the original consumer. Do not hold Pi's
|
|
@@ -50,6 +110,20 @@ async function providerErrorCode(response) {
|
|
|
50
110
|
reader.releaseLock();
|
|
51
111
|
}
|
|
52
112
|
}
|
|
113
|
+
/** Retry only typed transient failures, never usage exhaustion or rejection. */
|
|
114
|
+
export function isRetryableProviderRejection(rejection) {
|
|
115
|
+
if (rejection === undefined)
|
|
116
|
+
return false;
|
|
117
|
+
switch (rejection.code) {
|
|
118
|
+
case "provider_unavailable":
|
|
119
|
+
case "rate-limit":
|
|
120
|
+
case "overloaded":
|
|
121
|
+
return true;
|
|
122
|
+
case "usage-limit-reached":
|
|
123
|
+
case "provider_rejected":
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
53
127
|
/**
|
|
54
128
|
* Observe one call's typed HTTP and parsed-event evidence. Pi owns stream
|
|
55
129
|
* decoding; this module retains no provider prose.
|
|
@@ -69,15 +143,25 @@ export function observeProviderStatus(options) {
|
|
|
69
143
|
throw cause;
|
|
70
144
|
}
|
|
71
145
|
const status = response.status;
|
|
72
|
-
const code = status === 429
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
const code = status === 429 || status === 529
|
|
147
|
+
? await providerErrorKind(response)
|
|
148
|
+
: undefined;
|
|
149
|
+
const subscriptionRejected = status === 429 &&
|
|
150
|
+
Option.isSome(decodeRejected(response.headers.get("anthropic-ratelimit-unified-status")));
|
|
151
|
+
if (status < 400 || status > 599)
|
|
152
|
+
rejection = undefined;
|
|
153
|
+
else if (subscriptionRejected)
|
|
154
|
+
rejection = usageLimit(response.headers);
|
|
155
|
+
else if (code === usageLimitCode)
|
|
156
|
+
rejection = { code: "usage-limit-reached" };
|
|
157
|
+
else if (code === "overloaded_error")
|
|
158
|
+
rejection = { code: "overloaded" };
|
|
159
|
+
else if (status === 429 && code !== "insufficient_quota")
|
|
160
|
+
rejection = rateLimit(response.headers);
|
|
161
|
+
else if (transientStatus(status) && code !== "insufficient_quota")
|
|
162
|
+
rejection = { code: "provider_unavailable", status };
|
|
163
|
+
else
|
|
164
|
+
rejection = { code: "provider_rejected", status };
|
|
81
165
|
return response;
|
|
82
166
|
};
|
|
83
167
|
return {
|
|
@@ -87,8 +171,27 @@ export function observeProviderStatus(options) {
|
|
|
87
171
|
if (Option.isSome(failed) &&
|
|
88
172
|
failed.value.response.error.code === usageLimitCode)
|
|
89
173
|
rejection = { code: "usage-limit-reached" };
|
|
174
|
+
if (Option.isSome(decodeOverloaded(data)))
|
|
175
|
+
rejection = { code: "overloaded" };
|
|
90
176
|
await options?.onProviderStreamEvent?.(data, model);
|
|
91
177
|
},
|
|
92
|
-
rejection: () =>
|
|
178
|
+
rejection: (message) => {
|
|
179
|
+
if (rejection !== undefined)
|
|
180
|
+
return rejection;
|
|
181
|
+
// Pi throws Anthropic event:error before its parsed-event hook and
|
|
182
|
+
// retains that JSON envelope verbatim as the terminal errorMessage.
|
|
183
|
+
// Decode that maintained producer's representation, never match prose.
|
|
184
|
+
const text = message?.errorMessage;
|
|
185
|
+
if (message?.stopReason !== "error" ||
|
|
186
|
+
text === undefined ||
|
|
187
|
+
text.length > errorBodyBytes ||
|
|
188
|
+
new TextEncoder().encode(text).byteLength > errorBodyBytes)
|
|
189
|
+
return undefined;
|
|
190
|
+
const decoded = decodeErrorReply(text);
|
|
191
|
+
return Option.isSome(decoded) &&
|
|
192
|
+
decoded.value.error.type === "overloaded_error"
|
|
193
|
+
? { code: "overloaded" }
|
|
194
|
+
: undefined;
|
|
195
|
+
},
|
|
93
196
|
};
|
|
94
197
|
}
|
package/docs/api/effect.md
CHANGED
|
@@ -1091,6 +1091,14 @@ optional deferred?: DeferredHandle;
|
|
|
1091
1091
|
optional errorMessage?: string;
|
|
1092
1092
|
```
|
|
1093
1093
|
|
|
1094
|
+
##### providerError?
|
|
1095
|
+
|
|
1096
|
+
```ts
|
|
1097
|
+
optional providerError?: string;
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
Provider prose for the failed conversation turn, never a diagnostic.
|
|
1101
|
+
|
|
1094
1102
|
##### rawStopReason?
|
|
1095
1103
|
|
|
1096
1104
|
```ts
|
|
@@ -1912,6 +1920,12 @@ Re-exports [streamModel](index.md#streammodel)
|
|
|
1912
1920
|
|
|
1913
1921
|
---
|
|
1914
1922
|
|
|
1923
|
+
### isRetryableProviderRejection
|
|
1924
|
+
|
|
1925
|
+
Re-exports [isRetryableProviderRejection](index.md#isretryableproviderrejection)
|
|
1926
|
+
|
|
1927
|
+
---
|
|
1928
|
+
|
|
1915
1929
|
### observeProviderStatus
|
|
1916
1930
|
|
|
1917
1931
|
Re-exports [observeProviderStatus](index.md#observeproviderstatus)
|
package/docs/api/index.md
CHANGED
|
@@ -943,6 +943,15 @@ the run, or with the abort reason (an `AbortError` when the reason is not an
|
|
|
943
943
|
type ProviderRejection =
|
|
944
944
|
| Readonly<{
|
|
945
945
|
code: "usage-limit-reached";
|
|
946
|
+
resetAt?: number;
|
|
947
|
+
limit?: "five_hour" | "seven_day" | "seven_day_opus" | "seven_day_sonnet";
|
|
948
|
+
}>
|
|
949
|
+
| Readonly<{
|
|
950
|
+
code: "rate-limit";
|
|
951
|
+
retryAfterMs?: number;
|
|
952
|
+
}>
|
|
953
|
+
| Readonly<{
|
|
954
|
+
code: "overloaded";
|
|
946
955
|
}>
|
|
947
956
|
| Readonly<{
|
|
948
957
|
code: "provider_unavailable";
|
|
@@ -966,7 +975,7 @@ type ProviderStatusRecorder = Readonly<{
|
|
|
966
975
|
onProviderStreamEvent: NonNullable<
|
|
967
976
|
SimpleStreamOptions["onProviderStreamEvent"]
|
|
968
977
|
>;
|
|
969
|
-
rejection: () => ProviderRejection | undefined;
|
|
978
|
+
rejection: (message?) => ProviderRejection | undefined;
|
|
970
979
|
}>;
|
|
971
980
|
```
|
|
972
981
|
|
|
@@ -1040,6 +1049,26 @@ Validate a catalog model's opaque sampling fields before it enters Mule.
|
|
|
1040
1049
|
|
|
1041
1050
|
---
|
|
1042
1051
|
|
|
1052
|
+
### isRetryableProviderRejection()
|
|
1053
|
+
|
|
1054
|
+
```ts
|
|
1055
|
+
function isRetryableProviderRejection(rejection): boolean;
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
Retry only typed transient failures, never usage exhaustion or rejection.
|
|
1059
|
+
|
|
1060
|
+
#### Parameters
|
|
1061
|
+
|
|
1062
|
+
| Parameter | Type |
|
|
1063
|
+
| ----------- | -------------------------------------------------------- |
|
|
1064
|
+
| `rejection` | [`ProviderRejection`](#providerrejection) \| `undefined` |
|
|
1065
|
+
|
|
1066
|
+
#### Returns
|
|
1067
|
+
|
|
1068
|
+
`boolean`
|
|
1069
|
+
|
|
1070
|
+
---
|
|
1071
|
+
|
|
1043
1072
|
### observeProviderStatus()
|
|
1044
1073
|
|
|
1045
1074
|
```ts
|