@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 +3 -3
- package/src/authority.ts +3 -2
- package/src/run-recovery.test.ts +106 -0
- package/src/run-recovery.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/kernel-do",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
16
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
+
});
|
package/src/run-recovery.ts
CHANGED
|
@@ -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[],
|