@frockbot/kernel-do 0.3.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/kernel-do",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,8 +12,8 @@
12
12
  "typecheck": "tsc --noEmit -p tsconfig.json"
13
13
  },
14
14
  "dependencies": {
15
- "@frockbot/kernel-composition": "0.3.0",
16
- "@frockbot/kernel-contracts": "0.3.0",
15
+ "@frockbot/kernel-composition": "0.3.1",
16
+ "@frockbot/kernel-contracts": "0.3.1",
17
17
  "cordis": "4.0.0-rc.8"
18
18
  },
19
19
  "devDependencies": {
package/src/authority.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  eventsForFailedRun,
33
33
  latestModelRequestJournalState,
34
34
  planBotRunRecovery,
35
+ unresolvedModelRequestFailure,
35
36
  } from "./run-recovery.js";
36
37
  import {
37
38
  BotTurnReconciliationRequiredError,
@@ -283,7 +284,7 @@ export class BotDurableAuthority<Snapshot> {
283
284
  previous,
284
285
  events,
285
286
  modelState.status === "unresolved"
286
- ? `Model request "${modelState.request.request.requestId}" has no durable provider outcome`
287
+ ? unresolvedModelRequestFailure(events, modelState.request)
287
288
  : message,
288
289
  );
289
290
  throw new Error(message);
@@ -366,7 +367,7 @@ export class BotDurableAuthority<Snapshot> {
366
367
  previous,
367
368
  events,
368
369
  modelState.status === "unresolved"
369
- ? `Model request "${modelState.request.request.requestId}" has no durable provider outcome`
370
+ ? unresolvedModelRequestFailure(events, modelState.request)
370
371
  : message,
371
372
  );
372
373
  throw new Error(message);
@@ -0,0 +1,106 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { SessionEvent } from "@frockbot/kernel-contracts";
3
+ import {
4
+ latestModelRequestJournalState,
5
+ unresolvedModelRequestFailure,
6
+ } from "./run-recovery.js";
7
+
8
+ // Distributive, so each member of the union keeps its own fields: a bare
9
+ // `Omit` over the union collapses to the keys they all share.
10
+ type UnstampedEvent = SessionEvent extends infer Event
11
+ ? Event extends SessionEvent
12
+ ? Omit<Event, "seq" | "timestamp">
13
+ : never
14
+ : never;
15
+
16
+ /** Stamp a journal in order, the way `Session.append` would have. */
17
+ function journal(...events: UnstampedEvent[]): SessionEvent[] {
18
+ return events.map(
19
+ (event, index) =>
20
+ ({
21
+ ...event,
22
+ seq: index + 1,
23
+ timestamp: new Date(Date.UTC(2026, 8, 3, 0, 0, index)).toISOString(),
24
+ }) as SessionEvent,
25
+ );
26
+ }
27
+
28
+ const request: UnstampedEvent = {
29
+ type: "model/request",
30
+ turn: 1,
31
+ step: 1,
32
+ request: {
33
+ requestId: "request-1",
34
+ provider: "flock-ai",
35
+ model: "@flock/auto",
36
+ system: "Be concise.",
37
+ messages: [{ role: "user", content: "hello" }],
38
+ tools: [],
39
+ },
40
+ };
41
+
42
+ function reconciliationRequired(
43
+ requestId: string,
44
+ reason: string,
45
+ ): UnstampedEvent {
46
+ return {
47
+ type: "model/reconciliation-required",
48
+ turn: 1,
49
+ step: 1,
50
+ requestId,
51
+ reason,
52
+ };
53
+ }
54
+
55
+ function unresolved(...events: UnstampedEvent[]): string {
56
+ const stamped = journal(...events);
57
+ const state = latestModelRequestJournalState(stamped);
58
+ if (state.status !== "unresolved") {
59
+ throw new Error(`expected an unresolved request, got ${state.status}`);
60
+ }
61
+ return unresolvedModelRequestFailure(stamped, state.request);
62
+ }
63
+
64
+ describe("unresolvedModelRequestFailure", () => {
65
+ test("carries the Agent's journaled reason", () => {
66
+ expect(
67
+ unresolved(
68
+ request,
69
+ reconciliationRequired(
70
+ "request-1",
71
+ "Model response outcome is uncertain: Model response stream ended before a terminal marker",
72
+ ),
73
+ ),
74
+ ).toBe(
75
+ 'Model request "request-1" has no durable provider outcome: Model response outcome is uncertain: Model response stream ended before a terminal marker',
76
+ );
77
+ });
78
+
79
+ test("reads the last reason when the Turn was retried", () => {
80
+ expect(
81
+ unresolved(
82
+ request,
83
+ reconciliationRequired("request-1", "first attempt"),
84
+ reconciliationRequired("request-1", "second attempt"),
85
+ ),
86
+ ).toBe(
87
+ 'Model request "request-1" has no durable provider outcome: second attempt',
88
+ );
89
+ });
90
+
91
+ test("ignores a reason journaled against another request", () => {
92
+ expect(
93
+ unresolved(
94
+ request,
95
+ reconciliationRequired("request-0", "an earlier call"),
96
+ ),
97
+ ).toBe('Model request "request-1" has no durable provider outcome');
98
+ });
99
+
100
+ // A run wedged by isolate eviction never got as far as journaling a reason.
101
+ test("summarizes when the Agent journaled no reason", () => {
102
+ expect(unresolved(request)).toBe(
103
+ 'Model request "request-1" has no durable provider outcome',
104
+ );
105
+ });
106
+ });
@@ -61,6 +61,31 @@ export function latestModelRequestJournalState(
61
61
  return state;
62
62
  }
63
63
 
64
+ /**
65
+ * Why an unresolved Model request parked its run, in the operator's words
66
+ * where the Agent recorded them.
67
+ *
68
+ * The request id alone names *which* call is unsettled but not what went
69
+ * wrong, and the Agent's own reason is journaled on
70
+ * `model/reconciliation-required` — an event the chat projection drops. Read
71
+ * back here it reaches the banner the person is actually looking at.
72
+ */
73
+ export function unresolvedModelRequestFailure(
74
+ events: readonly SessionEvent[],
75
+ request: Extract<SessionEvent, { type: "model/request" }>,
76
+ ): string {
77
+ const requestId = request.request.requestId;
78
+ const summary = `Model request "${requestId}" has no durable provider outcome`;
79
+ const journaled = events.findLast(
80
+ (event) =>
81
+ event.type === "model/reconciliation-required" &&
82
+ event.requestId === requestId,
83
+ );
84
+ return journaled?.type === "model/reconciliation-required"
85
+ ? `${summary}: ${journaled.reason}`
86
+ : summary;
87
+ }
88
+
64
89
  export function planBotRunRecovery<Snapshot>(
65
90
  run: StoredRunV1<Snapshot>,
66
91
  latest: readonly SessionEvent[],