@frockbot/kernel-contracts 0.3.14 → 0.3.16
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/session.ts +2 -2
- package/src/tool-attachments.test.ts +3 -4
- 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/session.ts
CHANGED
|
@@ -216,8 +216,8 @@ export class Session {
|
|
|
216
216
|
* Resolved attachment bytes, keyed by content hash, held only while this
|
|
217
217
|
* Session is resident.
|
|
218
218
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
219
|
+
* A screenshot in the session log would multiply durable storage and prompt
|
|
220
|
+
* size even though the Bot authority pages and chunks large events, so an
|
|
221
221
|
* attachment records a Workspace path and a content hash and nothing else.
|
|
222
222
|
* A tool that produced the bytes offers them here, and the request derived
|
|
223
223
|
* while they are still held carries them to a model that can see images. On
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// Tool result attachments: what may be recorded, and what may not.
|
|
2
2
|
//
|
|
3
3
|
// The one rule worth a suite of its own is that resolved bytes are never
|
|
4
|
-
// durable.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// than trimming it somewhere further down.
|
|
4
|
+
// durable. A base64 screenshot recorded in the paged session log would still
|
|
5
|
+
// multiply durable storage and model-request size, so the decoder refuses
|
|
6
|
+
// `dataBase64` on the durable side rather than trimming it further down.
|
|
8
7
|
import { describe, expect, test } from "bun:test";
|
|
9
8
|
import {
|
|
10
9
|
decodeToolAttachmentsV1,
|
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,
|