@frockbot/kernel-contracts 0.3.13 → 0.3.15
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/src/model-invocation.ts +42 -2
- package/src/session.test.ts +20 -0
- package/src/types.ts +26 -0
package/package.json
CHANGED
package/src/model-invocation.ts
CHANGED
|
@@ -7,9 +7,49 @@ export interface DurableModelEffect {
|
|
|
7
7
|
request: NormalizedModelRequest;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export const MODEL_PROVIDER_FAILURE_REASON_MAX_LENGTH_V1 = 500;
|
|
11
|
+
|
|
12
|
+
export type ModelProviderFailureClassV1 = "transient" | "permanent" | "unknown";
|
|
13
|
+
|
|
14
|
+
/** Keep provider diagnostics useful without letting an envelope grow the log. */
|
|
15
|
+
export function boundedModelProviderReasonV1(reason: unknown): string {
|
|
16
|
+
const value =
|
|
17
|
+
typeof reason === "string" && reason.trim()
|
|
18
|
+
? reason.trim()
|
|
19
|
+
: "The model provider did not give a reason";
|
|
20
|
+
return value.slice(0, MODEL_PROVIDER_FAILURE_REASON_MAX_LENGTH_V1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A definitive provider failure raised before response bytes were emitted. */
|
|
24
|
+
export class ModelProviderFailureError extends Error {
|
|
25
|
+
readonly classification: ModelProviderFailureClassV1;
|
|
26
|
+
readonly providerReason: string;
|
|
27
|
+
readonly retryAfterMs?: number;
|
|
28
|
+
|
|
29
|
+
constructor(input: {
|
|
30
|
+
classification: ModelProviderFailureClassV1;
|
|
31
|
+
reason: unknown;
|
|
32
|
+
retryAfterMs?: number;
|
|
33
|
+
}) {
|
|
34
|
+
const reason = boundedModelProviderReasonV1(input.reason);
|
|
35
|
+
super(reason);
|
|
36
|
+
this.name = "ModelProviderFailureError";
|
|
37
|
+
this.classification = input.classification;
|
|
38
|
+
this.providerReason = reason;
|
|
39
|
+
if (
|
|
40
|
+
input.retryAfterMs !== undefined &&
|
|
41
|
+
Number.isSafeInteger(input.retryAfterMs) &&
|
|
42
|
+
input.retryAfterMs >= 0
|
|
43
|
+
) {
|
|
44
|
+
this.retryAfterMs = input.retryAfterMs;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @deprecated Providers should raise a classified failure instead. */
|
|
50
|
+
export class LlmEffectNotStartedError extends ModelProviderFailureError {
|
|
11
51
|
constructor(message: string) {
|
|
12
|
-
super(message);
|
|
52
|
+
super({ classification: "unknown", reason: message });
|
|
13
53
|
this.name = "LlmEffectNotStartedError";
|
|
14
54
|
}
|
|
15
55
|
}
|
package/src/session.test.ts
CHANGED
|
@@ -39,6 +39,26 @@ afterEach(async () => {
|
|
|
39
39
|
await Promise.all(roots.splice(0).map((root) => root.fiber.dispose()));
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
test("a model retry is a small exact durable session event", () => {
|
|
43
|
+
const event = {
|
|
44
|
+
type: "model/retry",
|
|
45
|
+
turn: 1,
|
|
46
|
+
step: 2,
|
|
47
|
+
attempt: 3,
|
|
48
|
+
classification: "transient",
|
|
49
|
+
delayMs: 1_000,
|
|
50
|
+
seq: 0,
|
|
51
|
+
timestamp,
|
|
52
|
+
} as const;
|
|
53
|
+
expect(decodeSessionEvent(event)).toEqual(event);
|
|
54
|
+
expect(() =>
|
|
55
|
+
decodeSessionEvent({ ...event, reason: "large envelope" }),
|
|
56
|
+
).toThrow(/invalid fields/);
|
|
57
|
+
expect(() =>
|
|
58
|
+
decodeSessionEvent({ ...event, classification: "maybe" }),
|
|
59
|
+
).toThrow(/classification is invalid/);
|
|
60
|
+
});
|
|
61
|
+
|
|
42
62
|
test("a Bot-isolate hook failure is an exact durable session event", () => {
|
|
43
63
|
const event = {
|
|
44
64
|
type: "package/hook-failed",
|
package/src/types.ts
CHANGED
|
@@ -308,6 +308,14 @@ export interface SessionEventMap {
|
|
|
308
308
|
requestId: string;
|
|
309
309
|
reason: string;
|
|
310
310
|
};
|
|
311
|
+
/** One small durable explanation for why the next model attempt waited. */
|
|
312
|
+
"model/retry": {
|
|
313
|
+
turn: number;
|
|
314
|
+
step: number;
|
|
315
|
+
attempt: number;
|
|
316
|
+
classification: "transient" | "permanent" | "unknown";
|
|
317
|
+
delayMs: number;
|
|
318
|
+
};
|
|
311
319
|
"model/reconciliation-required": {
|
|
312
320
|
turn: number;
|
|
313
321
|
step: number;
|
|
@@ -1272,6 +1280,24 @@ export function decodeSessionEvent(input: unknown): SessionEvent {
|
|
|
1272
1280
|
requestId();
|
|
1273
1281
|
eventString(event.reason, "session event.reason");
|
|
1274
1282
|
break;
|
|
1283
|
+
case "model/retry":
|
|
1284
|
+
requireEventKeys(
|
|
1285
|
+
event,
|
|
1286
|
+
keys("turn", "step", "attempt", "classification", "delayMs"),
|
|
1287
|
+
"session event",
|
|
1288
|
+
);
|
|
1289
|
+
turn();
|
|
1290
|
+
step();
|
|
1291
|
+
eventInteger(event.attempt, "session event.attempt", 2);
|
|
1292
|
+
if (
|
|
1293
|
+
event.classification !== "transient" &&
|
|
1294
|
+
event.classification !== "permanent" &&
|
|
1295
|
+
event.classification !== "unknown"
|
|
1296
|
+
) {
|
|
1297
|
+
throw new Error("session event.classification is invalid");
|
|
1298
|
+
}
|
|
1299
|
+
eventInteger(event.delayMs, "session event.delayMs", 0);
|
|
1300
|
+
break;
|
|
1275
1301
|
case "assistant/chunk":
|
|
1276
1302
|
requireEventKeys(
|
|
1277
1303
|
event,
|