@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/kernel-contracts",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -7,9 +7,49 @@ export interface DurableModelEffect {
7
7
  request: NormalizedModelRequest;
8
8
  }
9
9
 
10
- export class LlmEffectNotStartedError extends Error {
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
  }
@@ -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
- * The session event log is one Durable Object value and a screenshot in it
220
- * would be a durable record that grows past what the object can hold, so an
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. The session event log is one Durable Object value; a base64
5
- // screenshot recorded in it would be a record that grows past what the object
6
- // can hold, so the decoder refuses `dataBase64` on the durable side rather
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,