@vincemakes/kiso-core 0.11.0 → 0.13.0
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/governance/delivery.d.ts +15 -4
- package/dist/governance/delivery.js +15 -4
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/kernel/compaction.d.ts +3 -1
- package/dist/kernel/compaction.js +30 -3
- package/dist/kernel/event-log.d.ts +6 -1
- package/dist/kernel/event-log.js +7 -2
- package/dist/kernel/loop.d.ts +17 -7
- package/dist/kernel/loop.js +307 -97
- package/dist/kernel/mode.d.ts +10 -12
- package/dist/kernel/mode.js +10 -7
- package/dist/kernel/permission.d.ts +12 -3
- package/dist/kernel/permission.js +12 -3
- package/dist/kernel/project.d.ts +4 -2
- package/dist/kernel/project.js +11 -5
- package/dist/protocol/adapter.d.ts +3 -3
- package/dist/protocol/adapter.js +3 -3
- package/dist/protocol/events.d.ts +30 -12
- package/dist/protocol/extension.d.ts +1 -1
- package/dist/protocol/messages.d.ts +12 -3
- package/dist/protocol/messages.js +6 -1
- package/dist/tools/tool.d.ts +63 -18
- package/dist/tools/tool.js +10 -3
- package/package.json +2 -2
|
@@ -8,10 +8,21 @@
|
|
|
8
8
|
* the same events the loop yielded, so the verdict is replayable and the
|
|
9
9
|
* model's narration never participates in its own grading.
|
|
10
10
|
*
|
|
11
|
-
* Producers are
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Producers are named by the CALLER, in `DeliveryConfig.producers` — the
|
|
12
|
+
* hand-maintained set is the ONLY source of delivery truth. SC-1b removed
|
|
13
|
+
* `Tool.delivers`, the flag this note used to describe as the eventual
|
|
14
|
+
* replacement: it was declared on the contract and read by nothing, here
|
|
15
|
+
* or anywhere. Should a tool's own declaration ever supersede the caller's
|
|
16
|
+
* set, it enters as a new field with a wiring and a gate, never as a
|
|
17
|
+
* standing promise the code has not kept.
|
|
18
|
+
*
|
|
19
|
+
* The verdict counts producer calls that COMPLETED (non-error results).
|
|
20
|
+
* `claimedInText` is computed and REPORTED but does not enter `passed` —
|
|
21
|
+
* a caller that wants "claimed but not delivered" to fail combines the two
|
|
22
|
+
* itself (the evals fixture does). With `required: false`, text claiming
|
|
23
|
+
* delivery over zero producer calls passes here. The canonical lie —
|
|
24
|
+
* "generated-document" with zero producer calls and a clean completed
|
|
25
|
+
* terminal — is caught by that combination.
|
|
15
26
|
*
|
|
16
27
|
* In M3.5 the emission side (artifact URLs extracted from results) joins;
|
|
17
28
|
* today a completed producer IS the emission.
|
|
@@ -8,10 +8,21 @@
|
|
|
8
8
|
* the same events the loop yielded, so the verdict is replayable and the
|
|
9
9
|
* model's narration never participates in its own grading.
|
|
10
10
|
*
|
|
11
|
-
* Producers are
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Producers are named by the CALLER, in `DeliveryConfig.producers` — the
|
|
12
|
+
* hand-maintained set is the ONLY source of delivery truth. SC-1b removed
|
|
13
|
+
* `Tool.delivers`, the flag this note used to describe as the eventual
|
|
14
|
+
* replacement: it was declared on the contract and read by nothing, here
|
|
15
|
+
* or anywhere. Should a tool's own declaration ever supersede the caller's
|
|
16
|
+
* set, it enters as a new field with a wiring and a gate, never as a
|
|
17
|
+
* standing promise the code has not kept.
|
|
18
|
+
*
|
|
19
|
+
* The verdict counts producer calls that COMPLETED (non-error results).
|
|
20
|
+
* `claimedInText` is computed and REPORTED but does not enter `passed` —
|
|
21
|
+
* a caller that wants "claimed but not delivered" to fail combines the two
|
|
22
|
+
* itself (the evals fixture does). With `required: false`, text claiming
|
|
23
|
+
* delivery over zero producer calls passes here. The canonical lie —
|
|
24
|
+
* "generated-document" with zero producer calls and a clean completed
|
|
25
|
+
* terminal — is caught by that combination.
|
|
15
26
|
*
|
|
16
27
|
* In M3.5 the emission side (artifact URLs extracted from results) joins;
|
|
17
28
|
* today a completed producer IS the emission.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import type { Message } from "../protocol/messages.js";
|
|
|
21
21
|
/**
|
|
22
22
|
* Rough token estimate (chars/4 + structural overhead). Calibration-free on
|
|
23
23
|
* purpose: context economy only needs a stable MONOTONE proxy, not an exact
|
|
24
|
-
* count — the threshold absorbs the error (mauri ADR-0007).
|
|
24
|
+
* count — the threshold absorbs the error (mauri ADR-0007). The proxy's
|
|
25
|
+
* three-word contract, and the pins that hold it, are
|
|
26
|
+
* packages/core/tests/sc1b-estimator.test.ts.
|
|
25
27
|
*/
|
|
26
28
|
export declare function estimateTokens(messages: readonly Message[]): number;
|
|
@@ -17,16 +17,43 @@
|
|
|
17
17
|
* The model-generated half of context economy (the /compact summary layer)
|
|
18
18
|
* lives in kernel/summarize.ts.
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* What a non-text block (an image) contributes. There is no character count
|
|
22
|
+
* to proxy, and this module refuses to guess a provider's image
|
|
23
|
+
* tokenization; the figure exists so a block is never worth ZERO, which is
|
|
24
|
+
* what MONOTONE requires. Deliberately small: under-stating an image is a
|
|
25
|
+
* threshold that fires slightly late, over-stating it is one that fires on
|
|
26
|
+
* a conversation that was never large.
|
|
27
|
+
*/
|
|
28
|
+
const NON_TEXT_BLOCK_TOKENS = 8;
|
|
29
|
+
/**
|
|
30
|
+
* chars/4 over a `string | ContentBlock[]` content field.
|
|
31
|
+
*
|
|
32
|
+
* SC-1b ①: the array arm must be summed BLOCK BY BLOCK. Reading `.length`
|
|
33
|
+
* off it yields the block COUNT — a five-block 50 KB message scored ~2
|
|
34
|
+
* tokens, and the live microcompact threshold believed it.
|
|
35
|
+
*/
|
|
36
|
+
function contentTokens(content) {
|
|
37
|
+
if (typeof content === "string")
|
|
38
|
+
return Math.ceil(content.length / 4);
|
|
39
|
+
let tokens = 0;
|
|
40
|
+
for (const block of content) {
|
|
41
|
+
tokens += block.type === "text" ? Math.ceil(block.text.length / 4) : NON_TEXT_BLOCK_TOKENS;
|
|
42
|
+
}
|
|
43
|
+
return tokens;
|
|
44
|
+
}
|
|
20
45
|
/**
|
|
21
46
|
* Rough token estimate (chars/4 + structural overhead). Calibration-free on
|
|
22
47
|
* purpose: context economy only needs a stable MONOTONE proxy, not an exact
|
|
23
|
-
* count — the threshold absorbs the error (mauri ADR-0007).
|
|
48
|
+
* count — the threshold absorbs the error (mauri ADR-0007). The proxy's
|
|
49
|
+
* three-word contract, and the pins that hold it, are
|
|
50
|
+
* packages/core/tests/sc1b-estimator.test.ts.
|
|
24
51
|
*/
|
|
25
52
|
export function estimateTokens(messages) {
|
|
26
53
|
let total = 0;
|
|
27
54
|
for (const msg of messages) {
|
|
28
55
|
if (msg.role === "user") {
|
|
29
|
-
total +=
|
|
56
|
+
total += contentTokens(msg.content);
|
|
30
57
|
}
|
|
31
58
|
else if (msg.role === "assistant") {
|
|
32
59
|
for (const block of msg.blocks) {
|
|
@@ -37,7 +64,7 @@ export function estimateTokens(messages) {
|
|
|
37
64
|
}
|
|
38
65
|
}
|
|
39
66
|
else {
|
|
40
|
-
total +=
|
|
67
|
+
total += contentTokens(msg.content) + 10;
|
|
41
68
|
}
|
|
42
69
|
}
|
|
43
70
|
return total;
|
|
@@ -37,7 +37,12 @@ export declare class EventLog {
|
|
|
37
37
|
*/
|
|
38
38
|
append(ev: EventInput): Event;
|
|
39
39
|
get all(): readonly Event[];
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Incremental view for consumers that already saw `seq` and before —
|
|
42
|
+
* STRICTLY after, so a poll loop delivers each event exactly once. A
|
|
43
|
+
* consumer that has seen nothing passes -1. (SC-1b ④: this filtered
|
|
44
|
+
* `>= seq` and re-served the seam event on every poll.)
|
|
45
|
+
*/
|
|
41
46
|
since(seq: number): readonly Event[];
|
|
42
47
|
get lastSeq(): number;
|
|
43
48
|
}
|
package/dist/kernel/event-log.js
CHANGED
|
@@ -41,9 +41,14 @@ export class EventLog {
|
|
|
41
41
|
get all() {
|
|
42
42
|
return this.#events;
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Incremental view for consumers that already saw `seq` and before —
|
|
46
|
+
* STRICTLY after, so a poll loop delivers each event exactly once. A
|
|
47
|
+
* consumer that has seen nothing passes -1. (SC-1b ④: this filtered
|
|
48
|
+
* `>= seq` and re-served the seam event on every poll.)
|
|
49
|
+
*/
|
|
45
50
|
since(seq) {
|
|
46
|
-
return this.#events.filter((e) => e.seq
|
|
51
|
+
return this.#events.filter((e) => e.seq > seq);
|
|
47
52
|
}
|
|
48
53
|
get lastSeq() {
|
|
49
54
|
return this.#next - 1;
|
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* L2 — the ReAct loop. The kernel's only loop; everything else is harness.
|
|
3
3
|
*
|
|
4
4
|
* An async generator that yields every event as it happens (never buffers a
|
|
5
|
-
* turn into a list — the
|
|
6
|
-
* `terminal` event per run (ADR-0004).
|
|
5
|
+
* turn into a list — the reference implementation's failure), and converges
|
|
6
|
+
* on exactly one `terminal` event per run (ADR-0004).
|
|
7
7
|
*
|
|
8
8
|
* SINGLE TRUTH (Phase B): the loop holds ONE EventLog. Messages are never
|
|
9
9
|
* stored alongside it — every adapter call derives them via
|
|
@@ -100,13 +100,23 @@ export interface LoopConfig {
|
|
|
100
100
|
*/
|
|
101
101
|
readonly approvalVerdict?: (decisionId: string) => boolean | undefined;
|
|
102
102
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
103
|
+
* DEAD — accepted by the type, never read by this loop.
|
|
104
|
+
*
|
|
105
|
+
* It was the C group channel for the failed-receipt pause: the loop
|
|
106
|
+
* persisted `uncertain_pending` and awaited a human verdict. ADR-0038
|
|
107
|
+
* removed that pause (a complete receipt IS the outcome), and with it
|
|
108
|
+
* every read of this field — a failure is now simply recorded failed and
|
|
109
|
+
* the siblings run on. The runtime still passes both callbacks
|
|
110
|
+
* (runtime/run.ts), so supplying them is harmless and changes nothing.
|
|
111
|
+
*
|
|
112
|
+
* The crash window — started, no receipt — is the ONLY uncertainty left,
|
|
113
|
+
* and the runtime's recovery driver owns it (RESOLVE_UNCERTAIN in
|
|
114
|
+
* runtime/recovery-plan.ts), not the loop. Both fields are kept because
|
|
115
|
+
* the surface is frozen (ADR-0051); do not read them as live wiring.
|
|
107
116
|
*/
|
|
108
117
|
readonly resolveUncertainty?: (executionId: string) => Promise<"rerun" | "abandoned">;
|
|
109
|
-
/**
|
|
118
|
+
/** DEAD, as above — the uncertainty twin of `approvalVerdict`
|
|
119
|
+
* (round 4, adversarial). Never read by the loop since ADR-0038. */
|
|
110
120
|
readonly uncertaintyVerdict?: (executionId: string) => "rerun" | "abandoned" | undefined;
|
|
111
121
|
/**
|
|
112
122
|
* E1: the COMPOSED approval chain — the runtime composes the
|
package/dist/kernel/loop.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* L2 — the ReAct loop. The kernel's only loop; everything else is harness.
|
|
3
3
|
*
|
|
4
4
|
* An async generator that yields every event as it happens (never buffers a
|
|
5
|
-
* turn into a list — the
|
|
6
|
-
* `terminal` event per run (ADR-0004).
|
|
5
|
+
* turn into a list — the reference implementation's failure), and converges
|
|
6
|
+
* on exactly one `terminal` event per run (ADR-0004).
|
|
7
7
|
*
|
|
8
8
|
* SINGLE TRUTH (Phase B): the loop holds ONE EventLog. Messages are never
|
|
9
9
|
* stored alongside it — every adapter call derives them via
|
|
@@ -205,11 +205,33 @@ export async function* loop(config) {
|
|
|
205
205
|
// rejection un-consumed — the no-op keeps it from surfacing as an
|
|
206
206
|
// unhandled rejection while the race consumers still receive it.
|
|
207
207
|
void violatedP.catch(() => { });
|
|
208
|
+
// ── EC-1 ① — the DURABLE TURN COMMIT gate ──────────────────────────────
|
|
209
|
+
// Invariant 3 (COMMIT GATING): a commit-required handler never starts
|
|
210
|
+
// before this turn's stop is DURABLE. The gate RESOLVES exactly once per
|
|
211
|
+
// turn — at the commit, or at the void — and the waiter then reads
|
|
212
|
+
// `committed`. It deliberately does not reuse `violatedP`: that one
|
|
213
|
+
// REJECTS, and a rejection awaited here would be caught by the launch's
|
|
214
|
+
// catch and recorded as a launchError, failing the whole run for what is
|
|
215
|
+
// an ordinary void. A gate that resolves keeps the void path quiet.
|
|
216
|
+
// Both are reset per turn (below): turn N+1's calls wait for turn N+1's
|
|
217
|
+
// commit. Safe because the settle drains every launch before the turn
|
|
218
|
+
// advances, so no launch ever waits on a stale gate.
|
|
219
|
+
let committed = false;
|
|
220
|
+
let settleTurn = () => { };
|
|
221
|
+
let turnSettled = new Promise((res) => {
|
|
222
|
+
settleTurn = res;
|
|
223
|
+
});
|
|
208
224
|
// The ask gate: an ask's human resolution blocks the calls after it.
|
|
209
225
|
// The DECIDE chain serializes the decisions in CALL order, so the gate
|
|
210
226
|
// an ask installs is structurally in place before the successors decide.
|
|
227
|
+
// EC-1 ③: it must be installed THERE and not at the ask itself, even
|
|
228
|
+
// though the ask now happens later (post-commit): the gate is what a
|
|
229
|
+
// precommit-eligible sibling consults to know an ask was accepted ahead
|
|
230
|
+
// of it, and that has to be knowable BEFORE the commit. Each call
|
|
231
|
+
// captures the gate ahead of it and its own release (below) — one shared
|
|
232
|
+
// `askRelease` used to mean the first ask's resolution opened the SECOND
|
|
233
|
+
// ask's gate.
|
|
211
234
|
let askGate = Promise.resolve();
|
|
212
|
-
let askRelease = null;
|
|
213
235
|
// The decision chain: each launch's decide is chained onto the previous
|
|
214
236
|
// one's — the decides run in CALL order and the chain resolves to the
|
|
215
237
|
// call's verdict.
|
|
@@ -219,16 +241,57 @@ export async function* loop(config) {
|
|
|
219
241
|
// keys (the executionId comes from the drain, seq-stable).
|
|
220
242
|
let idSeq = log.all.length + 1;
|
|
221
243
|
const nextDecisionId = () => `d-${idSeq++}`;
|
|
244
|
+
// ── EC-1 ② / ③ — the FIFO EXCLUSIVE BARRIER ────────────────────────────
|
|
245
|
+
// Absence is the conservative truth: a tool that declares nothing is
|
|
246
|
+
// EXCLUSIVE, and the kernel serializes it. `concurrency: "shared"` is the
|
|
247
|
+
// only way to overlap, and it is a per-TOOL certificate the kernel
|
|
248
|
+
// enforces — never a per-call claim it could not police.
|
|
249
|
+
//
|
|
250
|
+
// The fence is installed at ACCEPTANCE (in `launch`, i.e. CALL order),
|
|
251
|
+
// not when a handler starts: that is what makes it FIFO. A later sibling
|
|
252
|
+
// — including a precommit-safe read — never overtakes an exclusive
|
|
253
|
+
// invocation accepted before it, so a read can never observe a
|
|
254
|
+
// half-written file. A read after a write therefore loses its latency
|
|
255
|
+
// win; safe overtaking and snapshot semantics stay future work.
|
|
256
|
+
let fence = Promise.resolve();
|
|
257
|
+
const sharedRunning = [];
|
|
258
|
+
/** Reserve this call's place in the FIFO: what it must await before
|
|
259
|
+
* running, and the release to call once its handler is done. */
|
|
260
|
+
const reserve = (tool) => {
|
|
261
|
+
const shared = tool?.effects?.concurrency === "shared";
|
|
262
|
+
let release;
|
|
263
|
+
const done = new Promise((res) => {
|
|
264
|
+
release = res;
|
|
265
|
+
});
|
|
266
|
+
// An exclusive invocation waits for the fence AND every shared call
|
|
267
|
+
// already accepted ahead of it — while it runs, it runs alone.
|
|
268
|
+
const wait = shared ? fence : Promise.all([fence, ...sharedRunning]).then(() => undefined);
|
|
269
|
+
if (shared)
|
|
270
|
+
sharedRunning.push(done);
|
|
271
|
+
else {
|
|
272
|
+
fence = done;
|
|
273
|
+
sharedRunning.length = 0;
|
|
274
|
+
}
|
|
275
|
+
return { wait, release };
|
|
276
|
+
};
|
|
222
277
|
const launch = (call) => {
|
|
223
278
|
execActive += 1;
|
|
279
|
+
const tool = registry.get(call.name);
|
|
280
|
+
const slot = reserve(tool);
|
|
281
|
+
// EC-1 ③ — this call's place in the ASK order, filled in by the
|
|
282
|
+
// decide chain (call order). `ahead` is the gate of an ask ACCEPTED
|
|
283
|
+
// BEFORE this call; `release` opens this call's own gate for its
|
|
284
|
+
// successors and stays a no-op unless this call asks. Per-call, so
|
|
285
|
+
// two asks in one turn queue honestly instead of sharing one release.
|
|
286
|
+
const askOrder = { ahead: Promise.resolve(), release: () => { } };
|
|
224
287
|
launches.push((async () => {
|
|
225
288
|
try {
|
|
226
|
-
await acquireWindow();
|
|
227
289
|
decideChain = decideChain.then(async () => {
|
|
228
290
|
const v = await decideCall(call, registry, hooks, { signal: signal ?? NEVER_ABORT, ...(config.sessionId !== undefined ? { sessionId: config.sessionId } : {}) }, log, config.resolveApproval, config.approvalVerdict, signal, config.approvalPolicy, nextDecisionId, pushExec);
|
|
291
|
+
askOrder.ahead = askGate;
|
|
229
292
|
if (v.action === "ask") {
|
|
230
293
|
askGate = new Promise((res) => {
|
|
231
|
-
|
|
294
|
+
askOrder.release = res;
|
|
232
295
|
});
|
|
233
296
|
}
|
|
234
297
|
return v;
|
|
@@ -240,33 +303,94 @@ export async function* loop(config) {
|
|
|
240
303
|
pushExec(verdict.result);
|
|
241
304
|
return;
|
|
242
305
|
}
|
|
306
|
+
// the conservative order: the calls AFTER an ask wait for its
|
|
307
|
+
// human resolution. The context may have changed when the
|
|
308
|
+
// human approves — which is why even a precommit-safe read
|
|
309
|
+
// accepted after an ask waits here rather than racing ahead.
|
|
310
|
+
await askOrder.ahead;
|
|
311
|
+
if (violated)
|
|
312
|
+
return;
|
|
243
313
|
if (verdict.action === "ask") {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
314
|
+
try {
|
|
315
|
+
// EC-1 ③ — THE POST-COMMIT ASK. A human must never
|
|
316
|
+
// be asked to approve a call whose turn then proves
|
|
317
|
+
// invalid, so the pause waits for this turn's OWN
|
|
318
|
+
// commit exactly like a handler does. On an
|
|
319
|
+
// uncommitted turn the call bails here having asked
|
|
320
|
+
// NOTHING and started nothing. (Before EC-1 the
|
|
321
|
+
// question was put to a person mid-stream, and a
|
|
322
|
+
// provider that violated the protocol after its stop
|
|
323
|
+
// made that person's "yes" authorize a turn the
|
|
324
|
+
// kernel then voided.)
|
|
325
|
+
await turnSettled;
|
|
326
|
+
if (!committed)
|
|
327
|
+
return;
|
|
328
|
+
// The human pause — abortable by a user abort OR a
|
|
329
|
+
// turn void (the violated promise). Post-commit a
|
|
330
|
+
// void can no longer reach this branch; the race
|
|
331
|
+
// stays as the cheap structural guarantee that it
|
|
332
|
+
// never could.
|
|
333
|
+
const decision = await Promise.race([
|
|
334
|
+
humanPause(call, verdict.decisionId, hooks, log, config.resolveApproval, config.approvalVerdict, signal, pushExec, verdict.speaker),
|
|
335
|
+
violatedP,
|
|
336
|
+
]);
|
|
337
|
+
if (violated)
|
|
338
|
+
return;
|
|
339
|
+
if (decision.action !== "allow") {
|
|
340
|
+
// the reason rides the denial — the human's words,
|
|
341
|
+
// or the honest "no approval flow configured".
|
|
342
|
+
pushExec(resultEvent(call, denialResult(decision.reason ?? "denied")));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
262
345
|
}
|
|
346
|
+
finally {
|
|
347
|
+
// The gate opens the moment the human's part is over
|
|
348
|
+
// — whatever the outcome, and on every bail path:
|
|
349
|
+
// successors wait for the VERDICT, never for this
|
|
350
|
+
// call's handler, and a turn that voids before the
|
|
351
|
+
// ask must not strand them.
|
|
352
|
+
askOrder.release();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (tool?.effects?.precommitSafe !== true) {
|
|
356
|
+
// EC-1 ① (invariant 3, COMMIT GATING): the handler waits
|
|
357
|
+
// for this turn's OWN commit. An uncommitted turn never
|
|
358
|
+
// reaches a handler — the call bails here having emitted
|
|
359
|
+
// NO started event, so it is clean, never uncertain
|
|
360
|
+
// (abort semantics). This is the whole of "an invalid
|
|
361
|
+
// turn never starts a commit-required tool handler".
|
|
362
|
+
//
|
|
363
|
+
// EC-1 ③(b) — THE PRECOMMIT LAUNCH RULE is the `else`
|
|
364
|
+
// this branch is guarded by: a call skips the commit
|
|
365
|
+
// gate iff its tool declares `precommitSafe` AND its
|
|
366
|
+
// authorization is ALREADY satisfied — an `allow`
|
|
367
|
+
// verdict, no human in the loop. Both halves are
|
|
368
|
+
// necessary: the certificate says the EXECUTION is
|
|
369
|
+
// harmless (read-only, free, local, universally), never
|
|
370
|
+
// that the authorization is unnecessary. Such a call may
|
|
371
|
+
// run on a turn that later voids; invariant 7 owns that
|
|
372
|
+
// outcome — the receipt is an honest fact and the turn
|
|
373
|
+
// stays uncommitted.
|
|
374
|
+
await turnSettled;
|
|
375
|
+
if (!committed)
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// EC-1 ③ (invariants 5 + 6): wait behind the FIFO barrier,
|
|
379
|
+
// and only THEN take a window slot. Waiting — for the
|
|
380
|
+
// commit, for a human, or for the barrier — consumes no
|
|
381
|
+
// slot: the window is an EXECUTION window, not a
|
|
382
|
+
// pending-invocation window, so four held writes can never
|
|
383
|
+
// starve a runnable sibling.
|
|
384
|
+
await slot.wait;
|
|
385
|
+
if (violated)
|
|
386
|
+
return;
|
|
387
|
+
await acquireWindow();
|
|
388
|
+
try {
|
|
389
|
+
await runLedgered(call, registry, hooks, { signal: signal ?? NEVER_ABORT, ...(config.sessionId !== undefined ? { sessionId: config.sessionId } : {}) }, signal, pushExec);
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
releaseWindow();
|
|
263
393
|
}
|
|
264
|
-
// the conservative order: the calls AFTER an ask wait for its human
|
|
265
|
-
// resolution (the askGate is the ask's pause promise —
|
|
266
|
-
// resolved by default, released by the ask branch above).
|
|
267
|
-
// The context may have changed when the human approves.
|
|
268
|
-
await askGate;
|
|
269
|
-
await runLedgered(call, registry, hooks, { signal: signal ?? NEVER_ABORT, ...(config.sessionId !== undefined ? { sessionId: config.sessionId } : {}) }, signal, pushExec);
|
|
270
394
|
}
|
|
271
395
|
catch (err) {
|
|
272
396
|
// The abort sentinel (a user cancel during the decide or
|
|
@@ -277,7 +401,12 @@ export async function* loop(config) {
|
|
|
277
401
|
launchError ??= err;
|
|
278
402
|
}
|
|
279
403
|
finally {
|
|
280
|
-
|
|
404
|
+
// The barrier and the ask gate are released on EVERY exit —
|
|
405
|
+
// a denial, an uncommitted turn, an abort — or the siblings
|
|
406
|
+
// queued behind this call would wait forever. (Releasing an
|
|
407
|
+
// already-open gate is a no-op: a promise resolves once.)
|
|
408
|
+
askOrder.release();
|
|
409
|
+
slot.release();
|
|
281
410
|
execActive -= 1;
|
|
282
411
|
}
|
|
283
412
|
})());
|
|
@@ -325,6 +454,25 @@ export async function* loop(config) {
|
|
|
325
454
|
// user_input, …) from the stream is a FORGERY and must never reach
|
|
326
455
|
// the log.
|
|
327
456
|
let forgedEvent = false;
|
|
457
|
+
// EC-1 ①: a SECOND stop voids the turn — and unlike the pre-EC-1 path
|
|
458
|
+
// (which appended both, leaving two stops in the log forever), NEITHER
|
|
459
|
+
// stop is persisted now. A deliberate improvement of that class.
|
|
460
|
+
let duplicateStop = false;
|
|
461
|
+
// EC-1 ①: the turn's stop, HELD in memory until the stream proves
|
|
462
|
+
// clean. It is neither appended NOR yielded here — yield order must
|
|
463
|
+
// equal append order (persistence-ownership.test.ts), so the hold
|
|
464
|
+
// covers both, and the commit below performs both together.
|
|
465
|
+
let heldStop = null;
|
|
466
|
+
// EC-1 ⑤ (the live void): the last durable event BEFORE this turn's
|
|
467
|
+
// model output — the previous turn's stop, the user input, or a
|
|
468
|
+
// microcompact boundary. Everything after it is this turn's draft,
|
|
469
|
+
// which is exactly the range a void must abandon.
|
|
470
|
+
const turnStart = log.lastSeq;
|
|
471
|
+
// EC-1 ①: this turn's commit gate — reset BEFORE any call can launch.
|
|
472
|
+
committed = false;
|
|
473
|
+
turnSettled = new Promise((res) => {
|
|
474
|
+
settleTurn = res;
|
|
475
|
+
});
|
|
328
476
|
while (true) {
|
|
329
477
|
// Area 4: the backoff is abortable — a cancel landing during a
|
|
330
478
|
// retry wait ends the run now, not after the backoff.
|
|
@@ -370,9 +518,24 @@ export async function* loop(config) {
|
|
|
370
518
|
break;
|
|
371
519
|
}
|
|
372
520
|
if (ev.type === "stop") {
|
|
521
|
+
// EC-1 ① — DURABLE TURN COMMIT. The stop is HELD, not
|
|
522
|
+
// persisted: before EC-1 it was appended the instant it
|
|
523
|
+
// arrived, so a stop could be durable while the stream
|
|
524
|
+
// was still running — and the recovery keyed "this turn
|
|
525
|
+
// committed" on exactly that durable stop. The two
|
|
526
|
+
// truths are now one: a durable compatible stop means
|
|
527
|
+
// THIS producer observed clean stream exhaustion.
|
|
528
|
+
if (sawStop) {
|
|
529
|
+
duplicateStop = true;
|
|
530
|
+
violated = true;
|
|
531
|
+
violatedReject();
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
373
534
|
sawStop = true;
|
|
374
535
|
lastStop = ev.reason;
|
|
375
536
|
stopCount += 1;
|
|
537
|
+
heldStop = ev;
|
|
538
|
+
continue;
|
|
376
539
|
}
|
|
377
540
|
// R-E 0.1.43: the append precedes the launch — the call's
|
|
378
541
|
// framework seq is assigned here; invocationSeq must never
|
|
@@ -430,78 +593,98 @@ export async function* loop(config) {
|
|
|
430
593
|
error: { code: "invalid_request", retryable: false, message: "provider emitted events after its stop event" },
|
|
431
594
|
};
|
|
432
595
|
}
|
|
433
|
-
else if (
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
596
|
+
else if (duplicateStop) {
|
|
597
|
+
// EC-1 ①: a second stop. The pre-EC-1 path appended BOTH and voided
|
|
598
|
+
// afterwards, so a duplicate-stop turn left two stops in the log
|
|
599
|
+
// forever; now neither is durable — the turn simply never commits.
|
|
600
|
+
voided = {
|
|
601
|
+
kind: "error",
|
|
602
|
+
error: { code: "invalid_request", retryable: false, message: `provider emitted ${stopCount + 1} stop events in one turn` },
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
else if (stopCount === 0) {
|
|
606
|
+
// Area 6: protocol anomalies are STRUCTURED ERRORS, never a default
|
|
607
|
+
// `completed`. EC-1 ①: ONE arm now, not two — the pre-EC-1 code
|
|
608
|
+
// duplicated this verdict across the pending/no-pending split, where
|
|
609
|
+
// it never differed.
|
|
610
|
+
voided = {
|
|
611
|
+
kind: "error",
|
|
612
|
+
error: { code: "invalid_request", retryable: false, message: "provider stream ended without a stop event" },
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
else if (pending.length > 0) {
|
|
616
|
+
// C group: STRUCTURAL COMPATIBILITY — a stop reason that cannot
|
|
617
|
+
// carry tool calls voids the turn. EC-1 ①: this is now the second
|
|
618
|
+
// half of the commit condition and runs BEFORE the commit, so an
|
|
619
|
+
// incompatible turn never persists its stop; combined with the
|
|
620
|
+
// commit gate its calls never became effects either (pre-EC-1 they
|
|
621
|
+
// had already launched, and the void could only let their receipts
|
|
622
|
+
// land after the fact).
|
|
623
|
+
switch (lastStop) {
|
|
624
|
+
case "tool_use":
|
|
625
|
+
case "function_call":
|
|
626
|
+
break; // compatible with complete calls — the executions proceed
|
|
627
|
+
case "max_tokens":
|
|
628
|
+
voided = { kind: "max_tokens" };
|
|
629
|
+
break;
|
|
630
|
+
case "abort":
|
|
631
|
+
voided = { kind: "aborted", by: "user" };
|
|
632
|
+
break;
|
|
633
|
+
case "error":
|
|
634
|
+
voided = {
|
|
635
|
+
kind: "error",
|
|
636
|
+
error: { code: "unknown", retryable: false, message: "provider stopped with an error" },
|
|
637
|
+
};
|
|
638
|
+
break;
|
|
639
|
+
default:
|
|
640
|
+
voided = {
|
|
641
|
+
kind: "error",
|
|
642
|
+
error: {
|
|
643
|
+
code: "invalid_request",
|
|
644
|
+
retryable: false,
|
|
645
|
+
message: `provider stopped with '${String(lastStop)}' with ${pending.length} tool call(s) launched`,
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
break;
|
|
452
649
|
}
|
|
453
650
|
}
|
|
454
|
-
// ──
|
|
455
|
-
//
|
|
456
|
-
|
|
651
|
+
// ── EC-1 ① — TURN COMMIT ──────────────────────────────────────────
|
|
652
|
+
// The held stop is persisted HERE and only here: iterator done (the
|
|
653
|
+
// stream loop broke cleanly) AND structurally compatible (above). The
|
|
654
|
+
// append and the yield happen together, in the loop's ordinary order,
|
|
655
|
+
// so the durable log gains no event and loses none — only the MOMENT
|
|
656
|
+
// moves, and the projection is byte-identical. What it buys:
|
|
657
|
+
//
|
|
658
|
+
// a durable compatible stop ⇔ this producer observed clean stream
|
|
659
|
+
// exhaustion.
|
|
660
|
+
//
|
|
661
|
+
// The gate is released either way: a committed turn frees its
|
|
662
|
+
// handlers, a voided turn frees them to bail with no started event.
|
|
663
|
+
// An abort with calls in flight abandons the turn BEFORE it commits:
|
|
664
|
+
// the aborted turn leaves NO durable stop, so its calls stay an
|
|
665
|
+
// UNCOMMITTED DRAFT the resume voids (⑤) rather than a committed batch
|
|
666
|
+
// no one will ever execute — which would strand the model's tool_use
|
|
667
|
+
// blocks with no results (the EC1-F1 dangling-pair class). A turn with
|
|
668
|
+
// NO calls has nothing to abandon and still ends on its own stop
|
|
669
|
+
// reason, the pre-EC-1 order.
|
|
670
|
+
if (voided === null && pending.length > 0 && aborted()) {
|
|
671
|
+
settleTurn();
|
|
457
672
|
yield await terminal({ kind: "aborted", by: "user" });
|
|
458
673
|
return;
|
|
459
674
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
error: { code: "invalid_request", retryable: false, message: `provider emitted ${stopCount} stop events in one turn` },
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
switch (lastStop) {
|
|
478
|
-
case "tool_use":
|
|
479
|
-
case "function_call":
|
|
480
|
-
break; // compatible with complete calls — the executions proceed
|
|
481
|
-
case "max_tokens":
|
|
482
|
-
voided = { kind: "max_tokens" };
|
|
483
|
-
break;
|
|
484
|
-
case "abort":
|
|
485
|
-
voided = { kind: "aborted", by: "user" };
|
|
486
|
-
break;
|
|
487
|
-
case "error":
|
|
488
|
-
voided = {
|
|
489
|
-
kind: "error",
|
|
490
|
-
error: { code: "unknown", retryable: false, message: "provider stopped with an error" },
|
|
491
|
-
};
|
|
492
|
-
break;
|
|
493
|
-
default:
|
|
494
|
-
voided = {
|
|
495
|
-
kind: "error",
|
|
496
|
-
error: {
|
|
497
|
-
code: "invalid_request",
|
|
498
|
-
retryable: false,
|
|
499
|
-
message: `provider stopped with '${String(lastStop)}' with ${pending.length} tool call(s) launched`,
|
|
500
|
-
},
|
|
501
|
-
};
|
|
502
|
-
break;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
675
|
+
if (voided === null && heldStop !== null) {
|
|
676
|
+
const full = log.append(heldStop);
|
|
677
|
+
if (hooks.onEvent)
|
|
678
|
+
await hooks.onEvent(full, {}).catch(() => { });
|
|
679
|
+
committed = true;
|
|
680
|
+
yield full;
|
|
681
|
+
}
|
|
682
|
+
settleTurn();
|
|
683
|
+
// A turn with no tool calls ends on its OWN stop reason (Phase B,
|
|
684
|
+
// Area 6) — never a blanket `completed`. Reached only once committed.
|
|
685
|
+
if (voided === null && pending.length === 0) {
|
|
686
|
+
yield await terminal(terminalForStop(lastStop));
|
|
687
|
+
return;
|
|
505
688
|
}
|
|
506
689
|
// ── The turn settles: a void fires the violated signal — the
|
|
507
690
|
// not-started executions bail (abort semantics — no started, no
|
|
@@ -526,6 +709,33 @@ export async function* loop(config) {
|
|
|
526
709
|
if (launchError !== null)
|
|
527
710
|
throw launchError;
|
|
528
711
|
if (voided !== null) {
|
|
712
|
+
// EC-1 ⑤ — THE LIVE VOID. ① means a voided turn's commit-required
|
|
713
|
+
// call never ran, so nothing answers the `tool_use` its
|
|
714
|
+
// tool_call_end already persisted. The run ends here on its error
|
|
715
|
+
// terminal, and the recovery driver will never see it: its first
|
|
716
|
+
// rule is "the open run reached its terminal". So the NEXT turn of
|
|
717
|
+
// the same session would send the model an assistant tool_use with
|
|
718
|
+
// no result — the provider-400 class, live rather than after a
|
|
719
|
+
// crash. Pre-EC-1 the streaming launch had already answered the
|
|
720
|
+
// pair; closing the destructive hole opened this one.
|
|
721
|
+
//
|
|
722
|
+
// The fix is the instrument the resume already uses, produced here
|
|
723
|
+
// instead: the loop is a SECOND PRODUCER of `model_output_abandoned`
|
|
724
|
+
// (an existing variant — no new protocol surface, the frozen event
|
|
725
|
+
// contract holds). It voids the whole draft range, exactly as
|
|
726
|
+
// ABANDON_DRAFT does, and only when a call is still pure intent —
|
|
727
|
+
// a call with a durable started is a FACT, and the same rule as
|
|
728
|
+
// recovery-plan.ts's `unexecuted`. Idempotent by construction: the
|
|
729
|
+
// marker becomes the last boundary, so no later resume derives a
|
|
730
|
+
// second draft over the same range.
|
|
731
|
+
if (log.all.some((e) => e.type === "tool_call_end" &&
|
|
732
|
+
e.seq > turnStart &&
|
|
733
|
+
!log.all.some((x) => x.type === "tool_execution_started" && x.callId === e.callId))) {
|
|
734
|
+
const marker = log.append({ type: "model_output_abandoned", voidFromSeq: turnStart, reason: "the turn was voided before it committed" });
|
|
735
|
+
if (hooks.onEvent)
|
|
736
|
+
await hooks.onEvent(marker, {}).catch(() => { });
|
|
737
|
+
yield marker;
|
|
738
|
+
}
|
|
529
739
|
yield await terminal(voided);
|
|
530
740
|
return;
|
|
531
741
|
}
|
package/dist/kernel/mode.d.ts
CHANGED
|
@@ -6,21 +6,19 @@
|
|
|
6
6
|
* - `visibleToolNames` — physical removal: the registry is subset() to these
|
|
7
7
|
* tools BEFORE the adapter is called. The model cannot call what it cannot
|
|
8
8
|
* see; no system-prompt overlay can make that guarantee.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* That is the whole profile. SC-1b: `systemOverlay`, `permissionDefault`,
|
|
11
|
+
* `compactionKeepExtra`, and `stopPredicate` were REMOVED at 0.12.0 by the
|
|
12
|
+
* SC-1 memo's adjudication — all four were declared here and read by
|
|
13
|
+
* nothing, in the kernel or above it. Two of them ("reserved, read by the
|
|
14
|
+
* harness when the semantics land") had been waiting since mauri ADR-0011
|
|
15
|
+
* for semantics that never landed. A field that describes an intention
|
|
16
|
+
* rather than a behavior is a promise the type system makes on the
|
|
17
|
+
* kernel's behalf and the kernel does not keep; the honest profile is the
|
|
18
|
+
* one member that is actually applied.
|
|
16
19
|
*/
|
|
17
|
-
import type { PermissionDecision } from "./permission.js";
|
|
18
20
|
export interface ModeProfile {
|
|
19
21
|
readonly name: string;
|
|
20
|
-
readonly systemOverlay?: string;
|
|
21
22
|
readonly visibleToolNames?: readonly string[];
|
|
22
|
-
readonly permissionDefault?: PermissionDecision;
|
|
23
|
-
readonly compactionKeepExtra?: readonly string[];
|
|
24
|
-
readonly stopPredicate?: string;
|
|
25
23
|
}
|
|
26
24
|
export declare function resolveModeProfile(modes: readonly ModeProfile[] | undefined, name: string | undefined): ModeProfile | undefined;
|
package/dist/kernel/mode.js
CHANGED
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
* - `visibleToolNames` — physical removal: the registry is subset() to these
|
|
7
7
|
* tools BEFORE the adapter is called. The model cannot call what it cannot
|
|
8
8
|
* see; no system-prompt overlay can make that guarantee.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* That is the whole profile. SC-1b: `systemOverlay`, `permissionDefault`,
|
|
11
|
+
* `compactionKeepExtra`, and `stopPredicate` were REMOVED at 0.12.0 by the
|
|
12
|
+
* SC-1 memo's adjudication — all four were declared here and read by
|
|
13
|
+
* nothing, in the kernel or above it. Two of them ("reserved, read by the
|
|
14
|
+
* harness when the semantics land") had been waiting since mauri ADR-0011
|
|
15
|
+
* for semantics that never landed. A field that describes an intention
|
|
16
|
+
* rather than a behavior is a promise the type system makes on the
|
|
17
|
+
* kernel's behalf and the kernel does not keep; the honest profile is the
|
|
18
|
+
* one member that is actually applied.
|
|
16
19
|
*/
|
|
17
20
|
export function resolveModeProfile(modes, name) {
|
|
18
21
|
if (!name || !modes)
|
|
@@ -5,9 +5,18 @@
|
|
|
5
5
|
* allow one call, deny with a reason fed back to the model, or defer to a
|
|
6
6
|
* human. The kernel's contract is only the decision shape; where decisions
|
|
7
7
|
* are stored (accept-for-session) is harness territory (PermissionStore,
|
|
8
|
-
* M2).
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* M2).
|
|
9
|
+
*
|
|
10
|
+
* `defer` IS A REAL PAUSE. The M1 note that once lived here — "defer is a
|
|
11
|
+
* deny with reason 'awaiting user'; the human-in-the-loop wiring arrives
|
|
12
|
+
* with the harness" — described the behavior ADR-0024 decision #3 replaced,
|
|
13
|
+
* and that ADR's own Context names this note as the thing it fixed (a
|
|
14
|
+
* denial "fakes the pause": the model sees a refusal and adjusts while no
|
|
15
|
+
* human ever decided). Shipped now: the loop persists `permission_requested`
|
|
16
|
+
* and AWAITS the approval channel in the same run frame, the human's
|
|
17
|
+
* verdict is persisted as `permission_decided`, and the pause survives a
|
|
18
|
+
* crash (kernel/loop.ts). With no approval channel configured the deferral
|
|
19
|
+
* degrades to an HONEST denial that says so.
|
|
11
20
|
*/
|
|
12
21
|
export type PermissionDecision = {
|
|
13
22
|
readonly action: "allow";
|
|
@@ -5,9 +5,18 @@
|
|
|
5
5
|
* allow one call, deny with a reason fed back to the model, or defer to a
|
|
6
6
|
* human. The kernel's contract is only the decision shape; where decisions
|
|
7
7
|
* are stored (accept-for-session) is harness territory (PermissionStore,
|
|
8
|
-
* M2).
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* M2).
|
|
9
|
+
*
|
|
10
|
+
* `defer` IS A REAL PAUSE. The M1 note that once lived here — "defer is a
|
|
11
|
+
* deny with reason 'awaiting user'; the human-in-the-loop wiring arrives
|
|
12
|
+
* with the harness" — described the behavior ADR-0024 decision #3 replaced,
|
|
13
|
+
* and that ADR's own Context names this note as the thing it fixed (a
|
|
14
|
+
* denial "fakes the pause": the model sees a refusal and adjusts while no
|
|
15
|
+
* human ever decided). Shipped now: the loop persists `permission_requested`
|
|
16
|
+
* and AWAITS the approval channel in the same run frame, the human's
|
|
17
|
+
* verdict is persisted as `permission_decided`, and the pause survives a
|
|
18
|
+
* crash (kernel/loop.ts). With no approval channel configured the deferral
|
|
19
|
+
* degrades to an HONEST denial that says so.
|
|
11
20
|
*/
|
|
12
21
|
/** The denial a tool result carries when a call was refused pre-flight. */
|
|
13
22
|
export function denialResult(reason) {
|
package/dist/kernel/project.d.ts
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
* re-runs the compaction algorithm (a future version could differ, A group/D
|
|
20
20
|
* group); `microcompacted` boundaries re-derive the cleared view from the
|
|
21
21
|
* stream itself (deterministic and idempotent); `summarized` (ADR-0044)
|
|
22
|
-
* replaces its covered range with one
|
|
23
|
-
*
|
|
22
|
+
* replaces its covered range with one USER message carrying the summary
|
|
23
|
+
* behind SUMMARY_FRAMING (E6's boundary honesty — see that constant
|
|
24
|
+
* below; it is deliberately NOT an assistant message). All three are
|
|
25
|
+
* persisted facts — the replay equals the live run. See ADR-0002.
|
|
24
26
|
*/
|
|
25
27
|
import type { Event } from "../protocol/events.js";
|
|
26
28
|
import type { EventInput } from "./event-log.js";
|
package/dist/kernel/project.js
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
* re-runs the compaction algorithm (a future version could differ, A group/D
|
|
20
20
|
* group); `microcompacted` boundaries re-derive the cleared view from the
|
|
21
21
|
* stream itself (deterministic and idempotent); `summarized` (ADR-0044)
|
|
22
|
-
* replaces its covered range with one
|
|
23
|
-
*
|
|
22
|
+
* replaces its covered range with one USER message carrying the summary
|
|
23
|
+
* behind SUMMARY_FRAMING (E6's boundary honesty — see that constant
|
|
24
|
+
* below; it is deliberately NOT an assistant message). All three are
|
|
25
|
+
* persisted facts — the replay equals the live run. See ADR-0002.
|
|
24
26
|
*/
|
|
25
27
|
/**
|
|
26
28
|
* C area: tools whose output is eligible for microcompact clearing — reads,
|
|
@@ -516,9 +518,13 @@ export function projectMessages(events) {
|
|
|
516
518
|
// its own range; the case documents the intent (and no flush:
|
|
517
519
|
// it must never split a message).
|
|
518
520
|
break;
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
521
|
+
// EC-1 (ruled in at the checkpoint): a SECOND `case
|
|
522
|
+
// "microcompacted"` sat here and was unreachable — the earlier
|
|
523
|
+
// case at the top of this switch owns the label, and in a
|
|
524
|
+
// JavaScript switch the first matching case wins. It never ran,
|
|
525
|
+
// it emitted a build warning ("this case clause will never be
|
|
526
|
+
// evaluated"), and it cost two lines of the kernel's budget.
|
|
527
|
+
// Removed; the microcompact replacement pass above is unchanged.
|
|
522
528
|
}
|
|
523
529
|
}
|
|
524
530
|
flushAssistant();
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* events carry `seq`). The three properties that let a ReAct loop stream
|
|
9
9
|
* without buffering and without losing its place.
|
|
10
10
|
*
|
|
11
|
-
* WHY not a `Promise<Event[]>`: a buffered adapter is what made
|
|
12
|
-
* streaming a second-class feature bolted onto
|
|
13
|
-
* shape IS the architecture — see ADR-0001.
|
|
11
|
+
* WHY not a `Promise<Event[]>`: a buffered adapter is what made the
|
|
12
|
+
* reference implementation's streaming a second-class feature bolted onto
|
|
13
|
+
* a batch loop. The contract shape IS the architecture — see ADR-0001.
|
|
14
14
|
*
|
|
15
15
|
* Adapters translate provider wire events INTO `Event`. They never see tool
|
|
16
16
|
* handlers (only `ToolSpec` projections) — the kernel is the only thing that
|
package/dist/protocol/adapter.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* events carry `seq`). The three properties that let a ReAct loop stream
|
|
9
9
|
* without buffering and without losing its place.
|
|
10
10
|
*
|
|
11
|
-
* WHY not a `Promise<Event[]>`: a buffered adapter is what made
|
|
12
|
-
* streaming a second-class feature bolted onto
|
|
13
|
-
* shape IS the architecture — see ADR-0001.
|
|
11
|
+
* WHY not a `Promise<Event[]>`: a buffered adapter is what made the
|
|
12
|
+
* reference implementation's streaming a second-class feature bolted onto
|
|
13
|
+
* a batch loop. The contract shape IS the architecture — see ADR-0001.
|
|
14
14
|
*
|
|
15
15
|
* Adapters translate provider wire events INTO `Event`. They never see tool
|
|
16
16
|
* handlers (only `ToolSpec` projections) — the kernel is the only thing that
|
|
@@ -220,10 +220,17 @@ export interface ToolExecutionSucceeded {
|
|
|
220
220
|
readonly tags?: readonly string[];
|
|
221
221
|
}
|
|
222
222
|
/**
|
|
223
|
-
* The side effect ran and FAILED.
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
223
|
+
* The side effect ran and FAILED. A complete receipt IS the outcome, so this
|
|
224
|
+
* execution is "failed" — NEVER "uncertain", whatever `safeToRetry` says
|
|
225
|
+
* (ADR-0038 superseding ADR-0025 decision #3: uncertainty belongs to the
|
|
226
|
+
* crash window alone — started, no receipt). The run does not pause here;
|
|
227
|
+
* siblings continue and the model retries with the error in hand, and that
|
|
228
|
+
* retry is a NEW call that re-passes the approval chain.
|
|
229
|
+
*
|
|
230
|
+
* `safeToRetry` is the tool's own `idempotent: true` declaration, carried
|
|
231
|
+
* for HISTORY: since ADR-0038 it no longer feeds the ledger's status
|
|
232
|
+
* derivation (runtime/ledger.ts). Its one live consequence is the honest
|
|
233
|
+
* note appended to a non-idempotent failure's result (ADR-0038 Amendment 1).
|
|
227
234
|
*/
|
|
228
235
|
export interface ToolExecutionFailed {
|
|
229
236
|
readonly seq: number;
|
|
@@ -294,11 +301,20 @@ export interface PermissionExpired {
|
|
|
294
301
|
readonly reason: string;
|
|
295
302
|
}
|
|
296
303
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* pause
|
|
304
|
+
* HISTORICAL — the current kernel NEVER emits this event.
|
|
305
|
+
*
|
|
306
|
+
* It marked the C group's failed-receipt pause: a non-idempotent execution
|
|
307
|
+
* that FAILED paused the run until a human ruled. ADR-0038 removed that
|
|
308
|
+
* pause (a complete receipt IS the outcome), so nothing appends this any
|
|
309
|
+
* more — pinned by `packages/core/tests/execution-gate.test.ts` and
|
|
310
|
+
* `packages/runtime/tests/execution.test.ts`, which assert its ABSENCE.
|
|
311
|
+
*
|
|
312
|
+
* The variant stays because the durable contract is frozen (ADR-0051) and
|
|
313
|
+
* old logs replay theirs verbatim (ADR-0038, Consequences): the projection
|
|
314
|
+
* renders nothing for it, and the ledger reports those receipted executions
|
|
315
|
+
* as "failed". The crash window — started, no receipt — is now the only
|
|
316
|
+
* source of uncertainty, and it is resolved through
|
|
317
|
+
* `uncertainExecutions()` / `resolveUncertain()`, not through this event.
|
|
302
318
|
*/
|
|
303
319
|
export interface UncertainPending {
|
|
304
320
|
readonly seq: number;
|
|
@@ -342,9 +358,11 @@ export interface MicroCompactEvent {
|
|
|
342
358
|
* range. `coversToSeq` is the seq of the LAST covered event; the covered
|
|
343
359
|
* range runs from just past the previous `summarized` event's coversToSeq
|
|
344
360
|
* (or the trajectory's start for the first) up to coversToSeq. The
|
|
345
|
-
* projection replaces exactly those events with ONE
|
|
346
|
-
*
|
|
347
|
-
*
|
|
361
|
+
* projection replaces exactly those events with ONE USER message carrying
|
|
362
|
+
* the summary behind a fixed framing line (E6's boundary honesty: the
|
|
363
|
+
* model reads compressed history as CONTEXT, never as a reply it produced
|
|
364
|
+
* — see SUMMARY_FRAMING in kernel/project.ts). It is not an assistant
|
|
365
|
+
* message; every `summarized` event always renders its own message. Byte-stable: a summarized event is a persisted fact, so the
|
|
348
366
|
* same events derive the same messages on every replay.
|
|
349
367
|
*
|
|
350
368
|
* The summary is generated OFF-LOOP through the session's own adapter —
|
|
@@ -83,7 +83,7 @@ export interface KisoExtension {
|
|
|
83
83
|
* E2: EXTEND the system prompt — append-only, never replace (a replace
|
|
84
84
|
* is a footgun; appends guarantee "adding an extension never removes
|
|
85
85
|
* existing guidance" — the monotonicity family of the approval chain's
|
|
86
|
-
* deny>
|
|
86
|
+
* deny > allow > ask and the veto short-circuit). The session's own
|
|
87
87
|
* systemPrompt comes first, then each extension's append in load order,
|
|
88
88
|
* \n\n-joined.
|
|
89
89
|
*/
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
* carries images. Forcing every caller into the array shape to serve the
|
|
11
11
|
* 5% that need vision is a tax paid on every line of product code.
|
|
12
12
|
*
|
|
13
|
-
* See ADR-0003 (sum type)
|
|
13
|
+
* See ADR-0003 (sum type). The tool contract's record is mauri ADR-0008 —
|
|
14
|
+
* the PREDECESSOR series, cited the same way in kernel/hooks.ts and
|
|
15
|
+
* kernel/mode.ts: kiso's own numbers 0006-0019 were NEVER assigned
|
|
16
|
+
* (docs/adrs/README.md), so an unprefixed "ADR-0008" sends a reader to a
|
|
17
|
+
* record that does not exist in this repo. The live tool contract is
|
|
18
|
+
* tools/tool.ts itself.
|
|
14
19
|
*
|
|
15
20
|
* This module is types-only: it compiles to nothing.
|
|
16
21
|
*/
|
|
@@ -107,8 +112,12 @@ export type Message = UserMessage | AssistantMessage | ToolResultMessage;
|
|
|
107
112
|
/**
|
|
108
113
|
* What the adapter advertises to the model.
|
|
109
114
|
*
|
|
110
|
-
* Deliberately minimal: the full tool (handler
|
|
111
|
-
* is L3-internal and an adapter
|
|
115
|
+
* Deliberately minimal: the full tool (the handler above all, plus the
|
|
116
|
+
* declarations the kernel keeps to itself) is L3-internal and an adapter
|
|
117
|
+
* must never see it. NB the historical list here named a `repair` member
|
|
118
|
+
* and a `dedup` member — neither exists on `Tool`: the (name, input) dedup
|
|
119
|
+
* guard was removed by ADR-0025, and repair means RECEIPT repair, which is
|
|
120
|
+
* the runtime's, not a tool's. An adapter that can reach
|
|
112
121
|
* the handler will eventually call it, and then the kernel is no longer the
|
|
113
122
|
* only thing that runs tools.
|
|
114
123
|
*/
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
* carries images. Forcing every caller into the array shape to serve the
|
|
11
11
|
* 5% that need vision is a tax paid on every line of product code.
|
|
12
12
|
*
|
|
13
|
-
* See ADR-0003 (sum type)
|
|
13
|
+
* See ADR-0003 (sum type). The tool contract's record is mauri ADR-0008 —
|
|
14
|
+
* the PREDECESSOR series, cited the same way in kernel/hooks.ts and
|
|
15
|
+
* kernel/mode.ts: kiso's own numbers 0006-0019 were NEVER assigned
|
|
16
|
+
* (docs/adrs/README.md), so an unprefixed "ADR-0008" sends a reader to a
|
|
17
|
+
* record that does not exist in this repo. The live tool contract is
|
|
18
|
+
* tools/tool.ts itself.
|
|
14
19
|
*
|
|
15
20
|
* This module is types-only: it compiles to nothing.
|
|
16
21
|
*/
|
package/dist/tools/tool.d.ts
CHANGED
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
* A tool is a pure declaration + handler pair. The kernel never branches on
|
|
5
5
|
* a tool's internals: `parameters` is a JSON Schema the kernel validates and
|
|
6
6
|
* projects to the adapter as `ToolSpec` (adapter never sees the handler —
|
|
7
|
-
* see ADR-0001)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* see ADR-0001).
|
|
8
|
+
*
|
|
9
|
+
* SC-1b: `concurrencySafe` and `delivers` were REMOVED at 0.12.0 by the
|
|
10
|
+
* SC-1 memo's adjudication — both were declared and consulted by nothing.
|
|
11
|
+
* Their absence is the honest contract; the concurrency RACE they were
|
|
12
|
+
* mistaken for a defense against is a real open question and moved to EC-1,
|
|
13
|
+
* which owes a mechanism that does not depend on a per-tool opt-in.
|
|
14
|
+
* Delivery truth is named by the CALLER (`DeliveryConfig.producers`), which
|
|
15
|
+
* is where it always actually lived — and since EC-1 that verdict lives in
|
|
16
|
+
* kiso-evals (governance/delivery.ts there), not in the kernel at all.
|
|
10
17
|
*
|
|
11
18
|
* WHY JSON Schema instead of a runtime library: the kernel has zero runtime
|
|
12
19
|
* dependencies (ADR-0001). Zod / TypeBox / valibot live at the harness layer;
|
|
@@ -49,24 +56,62 @@ export interface Tool<I = unknown> {
|
|
|
49
56
|
/** JSON Schema (draft-07 subset). Validated before execute. */
|
|
50
57
|
readonly parameters: Readonly<Record<string, unknown>>;
|
|
51
58
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
59
|
+
* The tool's own declaration that REPEATING its side effect is safe
|
|
60
|
+
* (reads, searches, pure computations).
|
|
61
|
+
*
|
|
62
|
+
* It is NOT a dedup guard. The kernel runs every logical call, including
|
|
63
|
+
* one whose (name, input) is identical to an earlier one — ADR-0024
|
|
64
|
+
* decision #2's (name, input) guard was REMOVED by ADR-0025 decision #1
|
|
65
|
+
* ("NO (name, input) dedup", kernel/loop.ts). Exactly-once is enforced
|
|
66
|
+
* by execution IDENTITY instead: a confirmed success is never
|
|
67
|
+
* re-executed (a lost model-facing result is repaired FROM the durable
|
|
68
|
+
* receipt), and an execution that STARTED without reporting — the crash
|
|
69
|
+
* window — waits for a human verdict. A FAILED execution is failed, not
|
|
70
|
+
* uncertain: a complete receipt IS the outcome (ADR-0038).
|
|
71
|
+
*
|
|
72
|
+
* What this flag actually decides, and nothing more:
|
|
73
|
+
* - a failure from a tool that did NOT declare it carries the honest
|
|
74
|
+
* "side effects may have partially applied; verify before retrying"
|
|
75
|
+
* note on the result (ADR-0038 Amendment 1);
|
|
76
|
+
* - it rides the durable receipt as `tool_execution_failed.safeToRetry`
|
|
77
|
+
* — history only, since ADR-0038 stopped it feeding ledger status.
|
|
78
|
+
*
|
|
79
|
+
* Undeclared is the safe side: unknown idempotency means the note
|
|
80
|
+
* applies. A retry is a NEW call and re-passes the approval chain.
|
|
56
81
|
*/
|
|
57
|
-
readonly
|
|
58
|
-
/** Marks this tool as an artifact producer (harness-side delivery truth). */
|
|
59
|
-
readonly delivers?: {
|
|
60
|
-
readonly kind: string;
|
|
61
|
-
};
|
|
82
|
+
readonly idempotent?: boolean;
|
|
62
83
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
84
|
+
* EC-1 — an OPTIMIZATION CERTIFICATE, never a safety claim.
|
|
85
|
+
*
|
|
86
|
+
* Read the type carefully: there is no `"exclusive"`, and there is no
|
|
87
|
+
* `false`. ABSENCE is the conservative truth — an undeclared tool is
|
|
88
|
+
* commit-required and exclusive — so the type system cannot express a
|
|
89
|
+
* claim that something unsafe is safe. Correctness comes from absence;
|
|
90
|
+
* a declaration only buys performance back. That is the whole reason
|
|
91
|
+
* this field is shaped so oddly, and it is the lesson SC-1b paid for:
|
|
92
|
+
* `concurrencySafe` and `delivers` were declarations the kernel never
|
|
93
|
+
* read, so they were fiction. The kernel ENFORCES both of these, in the
|
|
94
|
+
* same round that introduces them.
|
|
95
|
+
*
|
|
96
|
+
* precommitSafe — "running this before the turn commits is harmless:
|
|
97
|
+
* read-only AND free AND local, for EVERY invocation." Only such a
|
|
98
|
+
* call may start before Turn Commit, and only when its authorization
|
|
99
|
+
* is already satisfied (auto-allowed). Its execution never makes an
|
|
100
|
+
* uncommitted turn valid (invariant 7).
|
|
101
|
+
*
|
|
102
|
+
* concurrency: "shared" — "EVERY invocation may overlap every
|
|
103
|
+
* sibling." Without it the kernel serializes the tool behind a FIFO
|
|
104
|
+
* exclusive barrier, which is what closes the same-path write race:
|
|
105
|
+
* two edit_file calls on one path can no longer interleave.
|
|
106
|
+
*
|
|
107
|
+
* Per-call conflict granularity (a resourceKey) stays future and
|
|
108
|
+
* evidence-gated: this field is per-TOOL on purpose, because a per-call
|
|
109
|
+
* claim is exactly the kind the type system could not police.
|
|
68
110
|
*/
|
|
69
|
-
readonly
|
|
111
|
+
readonly effects?: {
|
|
112
|
+
readonly precommitSafe?: true;
|
|
113
|
+
readonly concurrency?: "shared";
|
|
114
|
+
};
|
|
70
115
|
/** R-C: ONE line for the system prompt — the tool's role, never the
|
|
71
116
|
* schema (the full description rides the JSON schema the provider
|
|
72
117
|
* transmits anyway — never pay twice). */
|
package/dist/tools/tool.js
CHANGED
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
* A tool is a pure declaration + handler pair. The kernel never branches on
|
|
5
5
|
* a tool's internals: `parameters` is a JSON Schema the kernel validates and
|
|
6
6
|
* projects to the adapter as `ToolSpec` (adapter never sees the handler —
|
|
7
|
-
* see ADR-0001)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* see ADR-0001).
|
|
8
|
+
*
|
|
9
|
+
* SC-1b: `concurrencySafe` and `delivers` were REMOVED at 0.12.0 by the
|
|
10
|
+
* SC-1 memo's adjudication — both were declared and consulted by nothing.
|
|
11
|
+
* Their absence is the honest contract; the concurrency RACE they were
|
|
12
|
+
* mistaken for a defense against is a real open question and moved to EC-1,
|
|
13
|
+
* which owes a mechanism that does not depend on a per-tool opt-in.
|
|
14
|
+
* Delivery truth is named by the CALLER (`DeliveryConfig.producers`), which
|
|
15
|
+
* is where it always actually lived — and since EC-1 that verdict lives in
|
|
16
|
+
* kiso-evals (governance/delivery.ts there), not in the kernel at all.
|
|
10
17
|
*
|
|
11
18
|
* WHY JSON Schema instead of a runtime library: the kernel has zero runtime
|
|
12
19
|
* dependencies (ADR-0001). Zod / TypeBox / valibot live at the harness layer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "kiso (foundation) core — protocol, event log, loop, hooks, modes, permissions, compaction, delivery truth. The 2,000-line kernel at the bottom of the kiso framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"openai"
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@vincemakes/kiso-evals": "0.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.13.0",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|