@frockbot/kernel-contracts 0.3.5 → 0.3.6

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.5",
3
+ "version": "0.3.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,6 +14,53 @@ export class LlmEffectNotStartedError extends Error {
14
14
  }
15
15
  }
16
16
 
17
+ /**
18
+ * A model request that ran out of time.
19
+ *
20
+ * Two deadlines, because they fail differently. `first-byte` is a provider that
21
+ * accepted the request and said nothing: the request may well be running, so
22
+ * the outcome is uncertain and the run settles on that. `idle` is a stream that
23
+ * started and then stopped mid-answer, which is the same uncertainty arriving
24
+ * later, with words already on screen.
25
+ *
26
+ * Either is a real answer where before there was none: a Turn with no deadline
27
+ * anywhere hung for seventeen minutes showing nothing at all.
28
+ */
29
+ export class ModelRequestDeadlineError extends Error {
30
+ constructor(
31
+ readonly phase: "first-byte" | "idle",
32
+ readonly milliseconds: number,
33
+ ) {
34
+ super(
35
+ phase === "first-byte"
36
+ ? `Model request produced nothing within ${Math.round(milliseconds / 1000)}s`
37
+ : `Model response stalled for ${Math.round(milliseconds / 1000)}s`,
38
+ );
39
+ this.name = "ModelRequestDeadlineError";
40
+ }
41
+ }
42
+
43
+ /** Deadlines a provider applies to one model request. */
44
+ export interface ModelRequestDeadlinesV1 {
45
+ /** Time allowed from sending the request to the first stream event. */
46
+ firstByteMs: number;
47
+ /** Time allowed between two stream events once the answer has started. */
48
+ idleMs: number;
49
+ }
50
+
51
+ /**
52
+ * The defaults every provider gets unless its Package names others.
53
+ *
54
+ * Two minutes to say anything at all is generous for a chat completion and
55
+ * still an order of magnitude inside the wall-clock a person will wait; the
56
+ * same allowance between chunks tolerates a slow tool-call assembly without
57
+ * tolerating a dead socket.
58
+ */
59
+ export const MODEL_REQUEST_DEADLINES_V1: ModelRequestDeadlinesV1 = {
60
+ firstByteMs: 120_000,
61
+ idleMs: 120_000,
62
+ };
63
+
17
64
  export type LlmReconciliationOutcome =
18
65
  | {
19
66
  status: "recovered";
package/src/session.ts CHANGED
@@ -424,10 +424,16 @@ export class Session {
424
424
  });
425
425
  }
426
426
  }
427
+ // An unresolved model request holds the step open — but only while the run
428
+ // might still resume and let that outcome land. Closing the turn means it
429
+ // never will, and a `turn/end` over an open step is itself invalid: it
430
+ // produced "turn 1 ended while step 1 is open" and left the log as unusable
431
+ // as the open turn it was meant to repair.
427
432
  if (
428
433
  openStep &&
429
- unresolvedModelRequests.size === 0 &&
430
- (closeTurn || !openStepHasAssistant)
434
+ (closeTurn
435
+ ? true
436
+ : unresolvedModelRequests.size === 0 && !openStepHasAssistant)
431
437
  ) {
432
438
  repairs.push({ type: "step/end", ...openStep, outcome: "interrupted" });
433
439
  }
package/src/types.ts CHANGED
@@ -170,7 +170,12 @@ export function turnEndReason(value: unknown): string | undefined {
170
170
  return bounded.length > 0 ? bounded : undefined;
171
171
  }
172
172
 
173
- /** The failure text a User sees for a Turn that did not complete. */
173
+ /**
174
+ * The failure text recorded against a Turn that did not complete. It names the
175
+ * outcome and the provider's own reason, which the debug surface and the API
176
+ * both need. It is a diagnostic, not copy: the client never renders it into the
177
+ * conversation — see `runFailureCopyV1` in the shell's client.
178
+ */
174
179
  export function turnFailureMessage(
175
180
  outcome: TurnOutcome,
176
181
  reason?: string,
@@ -370,6 +375,22 @@ export interface SessionEventMap {
370
375
  contentHash: string;
371
376
  generationId: string;
372
377
  };
378
+ /**
379
+ * A recorded Package effect intent that ended without its outcome: the host
380
+ * refused, or the attempt threw. Every `package/*-intent` closes with either
381
+ * its outcome event or this one, so the session log never says an effect was
382
+ * intended and then falls silent about how it ended — which is exactly what
383
+ * the intent/outcome pair is for (finding F12).
384
+ */
385
+ "package/effect-failed": {
386
+ turn: number;
387
+ step: number;
388
+ effectId: string;
389
+ effect: "author" | "undo" | "catalog-change";
390
+ reason: string;
391
+ /** The durable failure record, when the host wrote one. */
392
+ failureId?: string;
393
+ };
373
394
  /** A Bot-isolate loop hook failed open for one invocation. */
374
395
  "package/hook-failed": {
375
396
  packageId: string;
@@ -1327,6 +1348,36 @@ export function decodeSessionEvent(input: unknown): SessionEvent {
1327
1348
  eventString(event.contentHash, "session event.contentHash");
1328
1349
  eventString(event.generationId, "session event.generationId");
1329
1350
  break;
1351
+ case "package/effect-failed":
1352
+ requireEventKeys(
1353
+ event,
1354
+ keys(
1355
+ "turn",
1356
+ "step",
1357
+ "effectId",
1358
+ "effect",
1359
+ "reason",
1360
+ ...(Object.hasOwn(event, "failureId") ? ["failureId"] : []),
1361
+ ),
1362
+ "session event",
1363
+ );
1364
+ turn();
1365
+ step();
1366
+ eventString(event.effectId, "session event.effectId");
1367
+ if (
1368
+ event.effect !== "author" &&
1369
+ event.effect !== "undo" &&
1370
+ event.effect !== "catalog-change"
1371
+ ) {
1372
+ throw new Error(
1373
+ 'session event.effect must be "author", "undo" or "catalog-change"',
1374
+ );
1375
+ }
1376
+ eventString(event.reason, "session event.reason", true);
1377
+ if (Object.hasOwn(event, "failureId")) {
1378
+ eventString(event.failureId, "session event.failureId");
1379
+ }
1380
+ break;
1330
1381
  case "package/hook-failed":
1331
1382
  requireEventKeys(
1332
1383
  event,