@vincemakes/kiso-runtime 0.1.36 → 0.1.38
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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lock-adapter.d.ts +92 -0
- package/dist/lock-adapter.js +339 -0
- package/dist/recovery-plan.d.ts +74 -0
- package/dist/recovery-plan.js +209 -0
- package/dist/run.js +361 -321
- package/dist/store.d.ts +41 -33
- package/dist/store.js +69 -236
- package/package.json +1 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R-F 0.1.46 — the recovery plan: recovery as pure projection. From the
|
|
3
|
+
* durable event prefix the plan derives THE unique safe next step — never
|
|
4
|
+
* the adjudication itself (the approval pipeline stays in the runtime driver
|
|
5
|
+
* layer, run.ts). Purity: no I/O, no ID generation, no time — the same
|
|
6
|
+
* prefix always derives the same plan. The driver consumes one action at a
|
|
7
|
+
* time and re-derives after every append: the recovery is a loop over this
|
|
8
|
+
* projection, not a second state machine (the R-F thesis — fresh execution
|
|
9
|
+
* and resume walk the same ordinary program).
|
|
10
|
+
*
|
|
11
|
+
* The action vocabulary (the R-F directive):
|
|
12
|
+
* COMPLETED — no open run: nothing to recover
|
|
13
|
+
* TERMINAL — the open run reached its terminal
|
|
14
|
+
* WAIT_PERMISSION(seq) — a stored request awaits the human
|
|
15
|
+
* DECIDE_PERMISSION(seq) — a committed call re-enters the approval pipeline
|
|
16
|
+
* EXECUTE(seq) — a durable approval authorizes the persisted call
|
|
17
|
+
* RESOLVE_UNCERTAIN(id) — a started execution with no receipt: the crash
|
|
18
|
+
* window — the human decides (never auto-rerun)
|
|
19
|
+
* REPAIR_RESULT(id|seq) — the model-facing result is missing: complete it
|
|
20
|
+
* from the durable fact (the receipt or the denial)
|
|
21
|
+
* FILL_RESOLUTION(id) — a resolution's model-facing fill is missing
|
|
22
|
+
* ABANDON_DRAFT(from) — a text-bearing no-stop suffix: void it (the
|
|
23
|
+
* driver appends the marker AND expires the voided
|
|
24
|
+
* requests — one deterministic step, sentence 3)
|
|
25
|
+
* CONTINUE_MODEL — nothing left to repair: drive the loop
|
|
26
|
+
*
|
|
27
|
+
* The derivation order is the R-E recovery's phase order (the zero-behavior
|
|
28
|
+
* proof: the prefix-table gate and the healing fixtures run unchanged):
|
|
29
|
+
* terminal > completed > uncertain > draft > invocations (the Gap A calls,
|
|
30
|
+
* then the stored requests) > receipt repairs > resolution fills > continue.
|
|
31
|
+
*
|
|
32
|
+
* Inputs: `events` — the session's full event prefix (the log); `scope` —
|
|
33
|
+
* the open run's stored events at resume start (the run boundaries). Both
|
|
34
|
+
* are pure inputs; the caller loads them.
|
|
35
|
+
*/
|
|
36
|
+
import { executionLedger } from "./ledger.js";
|
|
37
|
+
/** The committed boundaries of a run's events (Gap B's boundary list). */
|
|
38
|
+
const isBoundary = (e) => e.type === "stop" ||
|
|
39
|
+
e.type === "user_input" ||
|
|
40
|
+
e.type === "terminal" ||
|
|
41
|
+
e.type === "microcompacted" ||
|
|
42
|
+
e.type === "compacted" ||
|
|
43
|
+
e.type === "summarized" ||
|
|
44
|
+
e.type === "model_output_abandoned";
|
|
45
|
+
/** A request's framework identity: its own invocationSeq, or the old-log
|
|
46
|
+
* fallback (the last same-callId call before the request, in the scope). */
|
|
47
|
+
export function invocationSeqOf(request, scope) {
|
|
48
|
+
if (request.invocationSeq !== undefined)
|
|
49
|
+
return request.invocationSeq;
|
|
50
|
+
let seq;
|
|
51
|
+
for (const e of scope) {
|
|
52
|
+
if (e.type === "tool_call_end" && e.callId === request.callId && e.seq < request.seq)
|
|
53
|
+
seq = e.seq;
|
|
54
|
+
}
|
|
55
|
+
return seq;
|
|
56
|
+
}
|
|
57
|
+
/** The events of the open run INCLUDING the recovery's own appends — the
|
|
58
|
+
* scope plus everything the driver appended after it (seq is global and
|
|
59
|
+
* monotonic, so the tail is exactly the appends). */
|
|
60
|
+
function openRunEvents(events, scope) {
|
|
61
|
+
if (scope.length === 0)
|
|
62
|
+
return events;
|
|
63
|
+
const lastScopeSeq = scope[scope.length - 1].seq;
|
|
64
|
+
const tail = events.filter((e) => e.seq > lastScopeSeq);
|
|
65
|
+
return [...scope, ...tail];
|
|
66
|
+
}
|
|
67
|
+
/** The one safe next step for the durable prefix (derivation order above). */
|
|
68
|
+
export function deriveRecoveryPlan(events, scope) {
|
|
69
|
+
// 1. the open run reached its terminal → done. The terminal may be in the
|
|
70
|
+
// scope (a resume adopted a run the loop completed in a previous
|
|
71
|
+
// process) or in the driver's own tail (the continuation's terminal).
|
|
72
|
+
if (openRunEvents(events, scope).some((e) => e.type === "terminal"))
|
|
73
|
+
return { kind: "TERMINAL" };
|
|
74
|
+
// 2. nothing open → nothing to recover.
|
|
75
|
+
if (scope.length === 0)
|
|
76
|
+
return { kind: "COMPLETED" };
|
|
77
|
+
// 3. the crash window: a started execution with no receipt is the human's
|
|
78
|
+
// (never auto-rerun — the prefix-table gate's row 7). The FIRST in log
|
|
79
|
+
// order blocks; the driver throws with the full list. A receipted
|
|
80
|
+
// execution is an outcome (ruling #12 / the α ruling: the audit keeps
|
|
81
|
+
// it) — never uncertain.
|
|
82
|
+
const uncertain = [...executionLedger(events).values()].filter((r) => r.status === "uncertain");
|
|
83
|
+
if (uncertain.length > 0)
|
|
84
|
+
return { kind: "RESOLVE_UNCERTAIN", executionId: uncertain[0].executionId };
|
|
85
|
+
// 4. Gap B: a text-bearing no-stop suffix is an abandoned draft — void it.
|
|
86
|
+
// Text-only detection (the 0.1.44 verification): a bare tool-call
|
|
87
|
+
// suffix is the legal approval-panel pause, never a draft. The
|
|
88
|
+
// boundary/draft scans run over the OPEN RUN's events INCLUDING the
|
|
89
|
+
// driver's own appends — the driver re-derives after every append,
|
|
90
|
+
// and the marker IT appended must already be the last boundary (the
|
|
91
|
+
// old one-pass Gap B never needed this: it ran before any append).
|
|
92
|
+
const openEvents = openRunEvents(events, scope);
|
|
93
|
+
const boundary = [...openEvents].reverse().find(isBoundary);
|
|
94
|
+
if (boundary !== undefined) {
|
|
95
|
+
const afterBoundary = openEvents.some((e) => (e.type === "text_delta" || e.type === "thinking") && e.seq > boundary.seq);
|
|
96
|
+
// The approval-panel pause: a suffix that carries a pending ask of the
|
|
97
|
+
// LIVE turn — the last boundary is the user_input, no stop since — is
|
|
98
|
+
// the human's pause, never a draft: the call was extracted and asked,
|
|
99
|
+
// the request is durable, the WAIT_PERMISSION step re-announces it.
|
|
100
|
+
// (The loop persists the stream's tail AFTER the pause resolves, so a
|
|
101
|
+
// crash mid-pause leaves exactly this shape.) A request AFTER a STOP
|
|
102
|
+
// is different: it is the draft's own ask (0143's shape) — the marker
|
|
103
|
+
// voids it and the request expires with the draft.
|
|
104
|
+
const liveAsk = afterBoundary &&
|
|
105
|
+
boundary.type === "user_input" &&
|
|
106
|
+
openEvents.some((e) => e.type === "permission_requested" && e.seq > boundary.seq);
|
|
107
|
+
if (afterBoundary && !liveAsk)
|
|
108
|
+
return { kind: "ABANDON_DRAFT", voidFromSeq: boundary.seq };
|
|
109
|
+
}
|
|
110
|
+
// 5. the invocations: the Gap A calls first (scope order), then the
|
|
111
|
+
// stored requests (scope order, then this recovery's own asks). Each:
|
|
112
|
+
// undecided → the pipeline must decide; decided-approved without an
|
|
113
|
+
// execution → execute the persisted call; decided-denied without a
|
|
114
|
+
// model-facing result → repair it from the denial.
|
|
115
|
+
const hasRequest = (callId, after) => events.some((e) => e.type === "permission_requested" && e.callId === callId && e.seq > after);
|
|
116
|
+
const hasExecution = (callId, after) => events.some((e) => e.type === "tool_execution_started" && e.callId === callId && e.seq > after);
|
|
117
|
+
const hasResult = (callId, after) => events.some((e) => e.type === "tool_result" && e.callId === callId && e.seq > after);
|
|
118
|
+
const decidedForCall = (callId, after) => {
|
|
119
|
+
for (const e of events) {
|
|
120
|
+
if (e.type !== "permission_decided")
|
|
121
|
+
continue;
|
|
122
|
+
// a durable POLICY verdict binds the call (E1); a human verdict
|
|
123
|
+
// binds its request, never the call (the requests pass owns it)
|
|
124
|
+
if (e.decidedBy !== undefined && e.callId === callId && e.seq > after)
|
|
125
|
+
return e;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
};
|
|
129
|
+
const decidedForRequest = (decisionId) => {
|
|
130
|
+
for (const e of events) {
|
|
131
|
+
if (e.type === "permission_decided" && e.decisionId === decisionId)
|
|
132
|
+
return e;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
};
|
|
136
|
+
for (const call of scope) {
|
|
137
|
+
if (call.type !== "tool_call_end")
|
|
138
|
+
continue;
|
|
139
|
+
// the boundary clause: a call whose turn has no legal stop is a
|
|
140
|
+
// DRAFT's call — Gap B voids it; this pass never touches it.
|
|
141
|
+
const turnEnd = scope.find((e) => e.type === "user_input" && e.seq > call.seq)?.seq ?? Number.POSITIVE_INFINITY;
|
|
142
|
+
if (!scope.some((e) => e.type === "stop" && e.seq > call.seq && e.seq < turnEnd))
|
|
143
|
+
continue;
|
|
144
|
+
// a stored request owns the invocation (the requests pass below) —
|
|
145
|
+
// "only a durable permission_decided authorizes an effect"; Gap A
|
|
146
|
+
// must never re-decide over a stored request.
|
|
147
|
+
if (hasRequest(call.callId, call.seq))
|
|
148
|
+
continue;
|
|
149
|
+
if (hasResult(call.callId, call.seq))
|
|
150
|
+
continue; // closed — nothing to fill
|
|
151
|
+
const decided = decidedForCall(call.callId, call.seq);
|
|
152
|
+
if (decided === undefined)
|
|
153
|
+
return { kind: "DECIDE_PERMISSION", invocationSeq: call.seq };
|
|
154
|
+
if (decided.decision === "approved") {
|
|
155
|
+
if (!hasExecution(call.callId, call.seq))
|
|
156
|
+
return { kind: "EXECUTE", invocationSeq: call.seq };
|
|
157
|
+
}
|
|
158
|
+
else if (!hasResult(call.callId, call.seq)) {
|
|
159
|
+
return { kind: "REPAIR_RESULT", invocationSeq: call.seq };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const lastScopeSeq = scope.length > 0 ? scope[scope.length - 1].seq : -1;
|
|
163
|
+
const requests = [
|
|
164
|
+
...scope.filter((e) => e.type === "permission_requested"),
|
|
165
|
+
// this recovery's own asks (the Gap A ask appends) — the log tail
|
|
166
|
+
...events.filter((e) => e.type === "permission_requested" && e.seq > lastScopeSeq),
|
|
167
|
+
];
|
|
168
|
+
for (const pending of requests) {
|
|
169
|
+
const invocationSeq = invocationSeqOf(pending, scope);
|
|
170
|
+
// a voided request was expired by the ABANDON_DRAFT step (sentence 3:
|
|
171
|
+
// never re-presented, never executed) — skip it here.
|
|
172
|
+
if (invocationSeq !== undefined &&
|
|
173
|
+
events.some((e) => e.type === "model_output_abandoned" && invocationSeq > e.voidFromSeq && invocationSeq <= e.seq)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const decided = decidedForRequest(pending.decisionId);
|
|
177
|
+
if (decided === undefined) {
|
|
178
|
+
return { kind: "WAIT_PERMISSION", invocationSeq: invocationSeq ?? pending.seq };
|
|
179
|
+
}
|
|
180
|
+
if (decided.decision === "approved") {
|
|
181
|
+
if (!hasExecution(pending.callId, pending.seq))
|
|
182
|
+
return { kind: "EXECUTE", invocationSeq: invocationSeq ?? pending.seq };
|
|
183
|
+
}
|
|
184
|
+
else if (!hasResult(pending.callId, pending.seq)) {
|
|
185
|
+
return { kind: "REPAIR_RESULT", invocationSeq: invocationSeq ?? pending.seq };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 6. the receipt repairs: an execution that reached a terminal state
|
|
189
|
+
// whose model-facing result never landed — complete it FROM THE
|
|
190
|
+
// RECEIPT, never re-executed.
|
|
191
|
+
for (const ev of scope) {
|
|
192
|
+
if (ev.type !== "tool_execution_succeeded" && ev.type !== "tool_execution_failed")
|
|
193
|
+
continue;
|
|
194
|
+
if (!hasResult(ev.callId, ev.seq) && !events.some((e) => e.type === "tool_result" && e.executionId === ev.executionId)) {
|
|
195
|
+
return { kind: "REPAIR_RESULT", executionId: ev.executionId };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// 7. the resolution fills: a persisted resolution whose model-facing fill
|
|
199
|
+
// never landed — the model must never stare at a dangling tool_use.
|
|
200
|
+
for (const ev of scope) {
|
|
201
|
+
if (ev.type !== "tool_execution_resolved")
|
|
202
|
+
continue;
|
|
203
|
+
if (!events.some((e) => e.type === "tool_result" && e.executionId === ev.executionId)) {
|
|
204
|
+
return { kind: "FILL_RESOLUTION", executionId: ev.executionId };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// 8. nothing left to repair — the loop drives from here.
|
|
208
|
+
return { kind: "CONTINUE_MODEL" };
|
|
209
|
+
}
|