@vincemakes/kiso-core 0.1.28 → 0.1.30
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 +1 -1
- package/dist/governance/delivery.js +2 -2
- package/dist/kernel/ledger.d.ts +1 -1
- package/dist/kernel/ledger.js +2 -2
- package/dist/kernel/loop.d.ts +7 -7
- package/dist/kernel/loop.js +49 -49
- package/dist/kernel/project.d.ts +6 -6
- package/dist/kernel/project.js +23 -23
- package/dist/protocol/adapter.d.ts +2 -2
- package/dist/protocol/adapter.js +1 -1
- package/dist/protocol/events.d.ts +15 -15
- package/dist/protocol/events.js +9 -9
- package/dist/protocol/extension.d.ts +3 -3
- package/dist/protocol/extension.js +1 -1
- package/dist/protocol/messages.d.ts +2 -2
- package/dist/tools/registry.d.ts +1 -1
- package/dist/tools/registry.js +2 -2
- package/dist/tools/tool.d.ts +1 -1
- package/package.json +3 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Producers are declared on tools (`delivers`, tools/tool.ts); the verdict
|
|
12
12
|
* counts producer calls that completed (non-error results), against a
|
|
13
|
-
* delivery claim in the text. The canonical lie — "
|
|
13
|
+
* delivery claim in the text. The canonical lie — "generated-document" with zero
|
|
14
14
|
* producer calls and a clean completed terminal — fails here.
|
|
15
15
|
*
|
|
16
16
|
* In M3.5 the emission side (artifact URLs extracted from results) joins;
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Producers are declared on tools (`delivers`, tools/tool.ts); the verdict
|
|
12
12
|
* counts producer calls that completed (non-error results), against a
|
|
13
|
-
* delivery claim in the text. The canonical lie — "
|
|
13
|
+
* delivery claim in the text. The canonical lie — "generated-document" with zero
|
|
14
14
|
* producer calls and a clean completed terminal — fails here.
|
|
15
15
|
*
|
|
16
16
|
* In M3.5 the emission side (artifact URLs extracted from results) joins;
|
|
17
17
|
* today a completed producer IS the emission.
|
|
18
18
|
*/
|
|
19
|
-
const CLAIM_PATTERN =
|
|
19
|
+
const CLAIM_PATTERN = /created|completed|delivered|done/i;
|
|
20
20
|
export function analyzeDelivery(events, config) {
|
|
21
21
|
const producerCalls = [];
|
|
22
22
|
const completedProducers = [];
|
package/dist/kernel/ledger.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* started, no terminal event yet → "uncertain" (interrupted: human)
|
|
19
19
|
* succeeded → "succeeded" (confirmed, never re-run)
|
|
20
20
|
* failed (any) → "failed" (a complete receipt IS
|
|
21
|
-
* the outcome —
|
|
21
|
+
* the outcome — ruling #12 / ADR-0038;
|
|
22
22
|
* safeToRetry stays on the event for
|
|
23
23
|
* history, it no longer feeds status)
|
|
24
24
|
* resolved "rerun" → "rerun" (human cleared it)
|
package/dist/kernel/ledger.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* started, no terminal event yet → "uncertain" (interrupted: human)
|
|
19
19
|
* succeeded → "succeeded" (confirmed, never re-run)
|
|
20
20
|
* failed (any) → "failed" (a complete receipt IS
|
|
21
|
-
* the outcome —
|
|
21
|
+
* the outcome — ruling #12 / ADR-0038;
|
|
22
22
|
* safeToRetry stays on the event for
|
|
23
23
|
* history, it no longer feeds status)
|
|
24
24
|
* resolved "rerun" → "rerun" (human cleared it)
|
|
@@ -50,7 +50,7 @@ export function executionLedger(events) {
|
|
|
50
50
|
if (prior) {
|
|
51
51
|
ledger.set(ev.executionId, {
|
|
52
52
|
...prior,
|
|
53
|
-
//
|
|
53
|
+
// ruling #12 (ADR-0038): a complete receipt IS the outcome —
|
|
54
54
|
// failed is "failed", never "uncertain"; uncertainty
|
|
55
55
|
// belongs to the crash window alone (started, no receipt).
|
|
56
56
|
status: "failed",
|
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
* assemble (onUserMessage / onPreLlm)
|
|
18
18
|
* → adapter.stream(): events yielded straight through; every validated
|
|
19
19
|
* and policy-allowed tool call LAUNCHES its execution immediately
|
|
20
|
-
* (
|
|
20
|
+
* (streaming execution) — the executions run concurrently under a window of 4
|
|
21
21
|
* (0.1.26, ADR-0024 Amd), their events queued and drained between
|
|
22
22
|
* stream events (completion order; the projection re-orders the
|
|
23
|
-
* results by call order —
|
|
23
|
+
* results by call order — the byte discipline)
|
|
24
24
|
* → the turn settles: the launched executions finish (receipts land
|
|
25
25
|
* before any terminal), the ask-gated successors follow the human's
|
|
26
|
-
* verdict (
|
|
26
|
+
* verdict (the conservative order)
|
|
27
27
|
* no tool calls / maxTurns / abort / max_tokens → terminal event, return
|
|
28
28
|
*
|
|
29
29
|
* Retry lives HERE and only here (ADR-0005): a retryable StructuredError
|
|
@@ -71,7 +71,7 @@ export interface LoopConfig {
|
|
|
71
71
|
readonly thresholdTokens: number;
|
|
72
72
|
};
|
|
73
73
|
/**
|
|
74
|
-
* C
|
|
74
|
+
* C area: MICROCOMPACT — when the projected context exceeds the threshold,
|
|
75
75
|
* append ONE durable `microcompacted` boundary (clearing compactable tool
|
|
76
76
|
* results older than the recent turns). The decision is a persisted fact:
|
|
77
77
|
* the projection derives the same cleared view from the same events,
|
|
@@ -93,20 +93,20 @@ export interface LoopConfig {
|
|
|
93
93
|
*/
|
|
94
94
|
readonly resolveApproval?: (decisionId: string) => Promise<PermissionDecision>;
|
|
95
95
|
/**
|
|
96
|
-
*
|
|
96
|
+
* round 4 (adversarial): a verdict the human ALREADY gave before an abort landed.
|
|
97
97
|
* The abort path consults this BEFORE yielding the aborted terminal: a
|
|
98
98
|
* consumed verdict must be recorded (exactly once), never lost — the
|
|
99
99
|
* human's decision outranks the abort.
|
|
100
100
|
*/
|
|
101
101
|
readonly approvalVerdict?: (decisionId: string) => boolean | undefined;
|
|
102
102
|
/**
|
|
103
|
-
* C
|
|
103
|
+
* C group: the channel that resolves a failed NON-idempotent execution.
|
|
104
104
|
* The loop persists `uncertain_pending`, yields it, and AWAITS the
|
|
105
105
|
* human verdict — no next model turn, no sibling tool, no auto-retry.
|
|
106
106
|
* Absent, the failure is recorded `abandoned` (never retried).
|
|
107
107
|
*/
|
|
108
108
|
readonly resolveUncertainty?: (executionId: string) => Promise<"rerun" | "abandoned">;
|
|
109
|
-
/**
|
|
109
|
+
/** round 4 (adversarial): the uncertainty twin of `approvalVerdict`. */
|
|
110
110
|
readonly uncertaintyVerdict?: (executionId: string) => "rerun" | "abandoned" | undefined;
|
|
111
111
|
/**
|
|
112
112
|
* E1: extension approval policies, tagged by their owning extension —
|
package/dist/kernel/loop.js
CHANGED
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
* assemble (onUserMessage / onPreLlm)
|
|
18
18
|
* → adapter.stream(): events yielded straight through; every validated
|
|
19
19
|
* and policy-allowed tool call LAUNCHES its execution immediately
|
|
20
|
-
* (
|
|
20
|
+
* (streaming execution) — the executions run concurrently under a window of 4
|
|
21
21
|
* (0.1.26, ADR-0024 Amd), their events queued and drained between
|
|
22
22
|
* stream events (completion order; the projection re-orders the
|
|
23
|
-
* results by call order —
|
|
23
|
+
* results by call order — the byte discipline)
|
|
24
24
|
* → the turn settles: the launched executions finish (receipts land
|
|
25
25
|
* before any terminal), the ask-gated successors follow the human's
|
|
26
|
-
* verdict (
|
|
26
|
+
* verdict (the conservative order)
|
|
27
27
|
* no tool calls / maxTurns / abort / max_tokens → terminal event, return
|
|
28
28
|
*
|
|
29
29
|
* Retry lives HERE and only here (ADR-0005): a retryable StructuredError
|
|
@@ -72,7 +72,7 @@ export async function* loop(config) {
|
|
|
72
72
|
};
|
|
73
73
|
const aborted = () => signal?.aborted === true;
|
|
74
74
|
// Assemble: the incoming user message may be rewritten or vetoed.
|
|
75
|
-
// C
|
|
75
|
+
// C group: the outcome is PERSISTED as a user_input_replaced event — the
|
|
76
76
|
// projection renders the final replacement AT THE INPUT'S POSITION (or
|
|
77
77
|
// nothing, for a true veto), so the rewritten fact is the ONLY fact
|
|
78
78
|
// every later turn of the run sees.
|
|
@@ -82,13 +82,13 @@ export async function* loop(config) {
|
|
|
82
82
|
const last = messages.at(-1);
|
|
83
83
|
if (last?.role === "user") {
|
|
84
84
|
const inputEvent = [...log.all].reverse().find((e) => e.type === "user_input");
|
|
85
|
-
//
|
|
85
|
+
// round 6: the hook runs AT MOST ONCE per input. A replacement that
|
|
86
86
|
// ALREADY exists (persisted before a crash, or before a resume)
|
|
87
87
|
// means the hook already spoke for this input — it must never
|
|
88
88
|
// run again, and the run continues from the durable fact.
|
|
89
89
|
const replacement = log.all.find((e) => e.type === "user_input_replaced" && e.replaces === inputEvent?.seq);
|
|
90
90
|
if (replacement !== undefined) {
|
|
91
|
-
//
|
|
91
|
+
// round 6/round 5 (P1-7): the hook ALREADY spoke for this input — a
|
|
92
92
|
// durable null content is a TRUE veto: restore the vetoed
|
|
93
93
|
// flag so the provider is NEVER called, even when earlier
|
|
94
94
|
// history exists (previously only an empty history happened
|
|
@@ -98,7 +98,7 @@ export async function* loop(config) {
|
|
|
98
98
|
}
|
|
99
99
|
if (replacement === undefined && inputEvent) {
|
|
100
100
|
const rewritten = await hooks.onUserMessage(last, {});
|
|
101
|
-
//
|
|
101
|
+
// round 1: the rewrite/veto is a NORMAL stream event — persisted by
|
|
102
102
|
// the harness and visible to consumers, never a hidden append.
|
|
103
103
|
const replaced = log.append({
|
|
104
104
|
type: "user_input_replaced",
|
|
@@ -109,11 +109,11 @@ export async function* loop(config) {
|
|
|
109
109
|
messages = derive();
|
|
110
110
|
yield replaced;
|
|
111
111
|
if (rewritten === null)
|
|
112
|
-
vetoed = true; //
|
|
112
|
+
vetoed = true; // round 3: a true veto ends the run
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
//
|
|
116
|
+
// round 3: a true veto ends the run — the provider is NEVER called, even
|
|
117
117
|
// when earlier history exists.
|
|
118
118
|
if (vetoed || messages.length === 0) {
|
|
119
119
|
yield await terminal({ kind: "completed" });
|
|
@@ -123,7 +123,7 @@ export async function* loop(config) {
|
|
|
123
123
|
// showed the sequential ledger is the bottleneck): the windowed parallel
|
|
124
124
|
// batching returns, this time with the ledger events emitted per call in
|
|
125
125
|
// deterministic order. The model stream and the tool executions run
|
|
126
|
-
// CONCURRENTLY (
|
|
126
|
+
// CONCURRENTLY (streaming execution): a tool_call_end validated and allowed by the
|
|
127
127
|
// policy chain launches its execution immediately; the events land
|
|
128
128
|
// through a queue the stream loop drains on every stream event — their
|
|
129
129
|
// seq order is the COMPLETION order (started/receipt/result land when
|
|
@@ -132,13 +132,13 @@ export async function* loop(config) {
|
|
|
132
132
|
// turn's results by CALL order (project.ts flushResults) — the
|
|
133
133
|
// completion order only affects the landing moment, never the derived
|
|
134
134
|
// messages. The window caps concurrent executions; the ask gate holds
|
|
135
|
-
// an ask AND the calls after it until the human decides (
|
|
135
|
+
// an ask AND the calls after it until the human decides (the conservative order — the
|
|
136
136
|
// context may have changed when the human approves); the STARTED event
|
|
137
137
|
// is acked by the drain so the handler never runs before its receipt is
|
|
138
138
|
// persisted (write-ahead preserved). A voided turn (forged event,
|
|
139
139
|
// post-stop violation, a non-compatible stop reason) fires the violated
|
|
140
|
-
// signal: started executions finish and their receipts land (
|
|
141
|
-
// receipt), not-started ones bail without a started event (abort
|
|
140
|
+
// signal: started executions finish and their receipts land (already-started executions still land their
|
|
141
|
+
// receipt), not-started ones bail without a started event (abort semantics —
|
|
142
142
|
// clean, never uncertain).
|
|
143
143
|
const WINDOW_SIZE = 4;
|
|
144
144
|
// The execution event queue. The drain (below) appends + yields each
|
|
@@ -195,7 +195,7 @@ export async function* loop(config) {
|
|
|
195
195
|
let launchError = null;
|
|
196
196
|
let violated = false;
|
|
197
197
|
// The violated signal: rejects when the turn is voided — the paused
|
|
198
|
-
// ask-branches bail (abort
|
|
198
|
+
// ask-branches bail (abort semantics for not-started executions). Typed
|
|
199
199
|
// `never` so the ask race resolves to the human decision alone.
|
|
200
200
|
let violatedReject = () => { };
|
|
201
201
|
const violatedP = new Promise((_, reject) => {
|
|
@@ -261,7 +261,7 @@ export async function* loop(config) {
|
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
-
//
|
|
264
|
+
// the conservative order: the calls AFTER an ask wait for its human
|
|
265
265
|
// resolution (the askGate is the ask's pause promise —
|
|
266
266
|
// resolved by default, released by the ask branch above).
|
|
267
267
|
// The context may have changed when the human approves.
|
|
@@ -293,7 +293,7 @@ export async function* loop(config) {
|
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
295
|
turns += 1;
|
|
296
|
-
// ── C
|
|
296
|
+
// ── C area: one-shot microcompact boundary when over the threshold ──
|
|
297
297
|
if (config.microcompact !== undefined && estimateTokens(messages) > config.microcompact.thresholdTokens) {
|
|
298
298
|
const beforeSeq = microcompactBoundarySeq(log.all, config.microcompact.keepResults ?? KEEP_COMPACTABLE_RESULTS);
|
|
299
299
|
if (beforeSeq !== undefined) {
|
|
@@ -316,11 +316,11 @@ export async function* loop(config) {
|
|
|
316
316
|
let stopCount = 0;
|
|
317
317
|
let streamed = false;
|
|
318
318
|
let attempts = 0;
|
|
319
|
-
//
|
|
319
|
+
// round 5: the turn is a strict protocol — once the provider stops, ANY
|
|
320
320
|
// further event (delta, tool call, usage, thinking) is a violation.
|
|
321
321
|
let sawStop = false;
|
|
322
322
|
let postStopViolation = false;
|
|
323
|
-
//
|
|
323
|
+
// round 5: the adapter may only produce its OWN event kinds — a
|
|
324
324
|
// kernel-owned event (terminal, tool_execution_*, permission_*,
|
|
325
325
|
// user_input, …) from the stream is a FORGERY and must never reach
|
|
326
326
|
// the log.
|
|
@@ -346,10 +346,10 @@ export async function* loop(config) {
|
|
|
346
346
|
streamed = true;
|
|
347
347
|
// 0.1.26: the launched executions' events land first —
|
|
348
348
|
// the completion order; the projection re-orders the
|
|
349
|
-
// results by call order (
|
|
349
|
+
// results by call order (the byte discipline).
|
|
350
350
|
for await (const q of drainExec())
|
|
351
351
|
yield q;
|
|
352
|
-
//
|
|
352
|
+
// round 5: the trust gate — a kernel-owned event from the
|
|
353
353
|
// adapter is a forgery: it is never appended (never
|
|
354
354
|
// persisted), and the turn ends with a unique
|
|
355
355
|
// invalid_request terminal below.
|
|
@@ -359,7 +359,7 @@ export async function* loop(config) {
|
|
|
359
359
|
violatedReject();
|
|
360
360
|
break;
|
|
361
361
|
}
|
|
362
|
-
//
|
|
362
|
+
// round 5: a delta/tool call/usage arriving AFTER the provider's
|
|
363
363
|
// stop is a protocol error — the violating event is never
|
|
364
364
|
// appended, and the turn ends with an error terminal (the
|
|
365
365
|
// pending tools must NOT execute).
|
|
@@ -376,7 +376,7 @@ export async function* loop(config) {
|
|
|
376
376
|
}
|
|
377
377
|
if (ev.type === "tool_call_end") {
|
|
378
378
|
pending.push(ev);
|
|
379
|
-
//
|
|
379
|
+
// streaming execution: the call launches immediately — the decide
|
|
380
380
|
// and the ledgered run proceed in parallel with the
|
|
381
381
|
// model stream.
|
|
382
382
|
launch(ev);
|
|
@@ -408,12 +408,12 @@ export async function* loop(config) {
|
|
|
408
408
|
return;
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
-
// ── The voided-terminal computation (
|
|
411
|
+
// ── The voided-terminal computation (round 5 / C group) ────────────────────
|
|
412
412
|
// A forged kernel-owned event, a post-stop event, or a
|
|
413
413
|
// non-compatible stop reason voids the turn: the launched
|
|
414
|
-
// executions still finish and their receipts land (
|
|
414
|
+
// executions still finish and their receipts land (already-started executions still land their
|
|
415
415
|
// receipt — the side effects happened), the not-started bail
|
|
416
|
-
// (abort
|
|
416
|
+
// (abort semantics), then the terminal.
|
|
417
417
|
let voided = null;
|
|
418
418
|
if (forgedEvent) {
|
|
419
419
|
voided = {
|
|
@@ -454,7 +454,7 @@ export async function* loop(config) {
|
|
|
454
454
|
yield await terminal({ kind: "aborted", by: "user" });
|
|
455
455
|
return;
|
|
456
456
|
}
|
|
457
|
-
// ── C
|
|
457
|
+
// ── C group: the turn is verified. 0.1.26 (streaming execution): the calls were
|
|
458
458
|
// ALREADY launched at tool_call_end, so a non-compatible stop reason
|
|
459
459
|
// VOIDS the turn instead of preventing the execution.
|
|
460
460
|
if (voided === null && pending.length > 0) {
|
|
@@ -501,9 +501,9 @@ export async function* loop(config) {
|
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
503
|
// ── The turn settles: a void fires the violated signal — the
|
|
504
|
-
// not-started executions bail (abort
|
|
504
|
+
// not-started executions bail (abort semantics — no started, no
|
|
505
505
|
// receipt, never uncertain); the started ones finish and their
|
|
506
|
-
// receipts land BEFORE the terminal or the next turn (
|
|
506
|
+
// receipts land BEFORE the terminal or the next turn (already-started executions still land their
|
|
507
507
|
// receipt). The drain yields every queued event; the STARTED
|
|
508
508
|
// ack resolves as the consumer persists (write-ahead), so the
|
|
509
509
|
// launches advance DURING the drain — a 10ms settle poll covers
|
|
@@ -578,7 +578,7 @@ function terminalForStop(reason) {
|
|
|
578
578
|
}
|
|
579
579
|
}
|
|
580
580
|
/** The tool_result event for a call — the shared shape (executionId rides
|
|
581
|
-
* it as the durable correlation,
|
|
581
|
+
* it as the durable correlation, round 5). */
|
|
582
582
|
function resultEvent(call, result, executionId) {
|
|
583
583
|
return {
|
|
584
584
|
type: "tool_result",
|
|
@@ -589,7 +589,7 @@ function resultEvent(call, result, executionId) {
|
|
|
589
589
|
// the runtime guard keeps a JS tool's illegal combination out of
|
|
590
590
|
// the persisted event too).
|
|
591
591
|
...(result.isError && result.errorKind ? { errorKind: result.errorKind } : {}),
|
|
592
|
-
//
|
|
592
|
+
// round 5: a live tool's tags are preserved losslessly (do-not-compact,
|
|
593
593
|
// billing receipts, trace anchors) — never dropped at the loop.
|
|
594
594
|
...(result.tags !== undefined ? { tags: result.tags } : {}),
|
|
595
595
|
...(executionId !== undefined ? { executionId } : {}),
|
|
@@ -634,7 +634,7 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
634
634
|
throw ABORTED;
|
|
635
635
|
// ── E1: the extension policy chain, decided BEFORE the human flow ─────
|
|
636
636
|
// A durable POLICY decision for THIS call takes effect on resume — the
|
|
637
|
-
// chain never re-runs when its verdict is already in the log (
|
|
637
|
+
// chain never re-runs when its verdict is already in the log (isomorphic
|
|
638
638
|
// alreadyReplaced: the persisted fact speaks for the call). The match is
|
|
639
639
|
// the same logical call: same callId, decidedBy set (a policy verdict,
|
|
640
640
|
// never a human's), and input identical to the original tool_call_end —
|
|
@@ -660,7 +660,7 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
660
660
|
v = await raceAbort(Promise.resolve(policy.decide(payload, ctx)), signal);
|
|
661
661
|
}
|
|
662
662
|
catch {
|
|
663
|
-
v = { action: "ask" }; //
|
|
663
|
+
v = { action: "ask" }; // a throwing policy counts as ask — it speaks, never silently
|
|
664
664
|
}
|
|
665
665
|
if (v.action === "abstain")
|
|
666
666
|
continue; // no opinion — not a verdict
|
|
@@ -678,10 +678,10 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
678
678
|
chainVerdict = { action: "deny", reason: deniedReason ?? "denied" };
|
|
679
679
|
}
|
|
680
680
|
else if (chainVerdict === undefined && anySpoke) {
|
|
681
|
-
chainVerdict = { action: "allow" }; //
|
|
681
|
+
chainVerdict = { action: "allow" }; // every speaker allows
|
|
682
682
|
}
|
|
683
683
|
else if (chainVerdict === undefined) {
|
|
684
|
-
//
|
|
684
|
+
// an all-abstain (ADR-0042): NO policy speaks — the call falls to
|
|
685
685
|
// the ask flow below, never to a silent auto-approve. The human
|
|
686
686
|
// decides; absent a channel, humanPause's honest denial.
|
|
687
687
|
chainVerdict = { action: "ask" };
|
|
@@ -707,7 +707,7 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
707
707
|
return { action: "deny", result: resultEvent(call, denialResult(durable.reason ?? "denied")) };
|
|
708
708
|
}
|
|
709
709
|
if (chainVerdict?.action === "ask") {
|
|
710
|
-
//
|
|
710
|
+
// ruling A (the E1 ask semantics fix): an ask means "a HUMAN must decide" — it
|
|
711
711
|
// routes DIRECTLY to the human approval pause, never through
|
|
712
712
|
// onPreTool: a static automated policy (e.g. the CLI's default deny
|
|
713
713
|
// for unknown tools) must not answer for the human. No approval
|
|
@@ -718,10 +718,10 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
718
718
|
}
|
|
719
719
|
return { action: "ask", decisionId: nextDecisionId() };
|
|
720
720
|
}
|
|
721
|
-
// Permission negotiation — defer is a REAL pause (Phase D). C
|
|
721
|
+
// Permission negotiation — defer is a REAL pause (Phase D). C group: the
|
|
722
722
|
// hook itself is cancelable (a slow policy query must not outlive an
|
|
723
723
|
// abort), and the signal is re-checked after it returns. Runs only when
|
|
724
|
-
// the policy chain did not run at all (
|
|
724
|
+
// the policy chain did not run at all (ruling A: an ask was already
|
|
725
725
|
// resolved by the human pause above — the static hook never speaks for
|
|
726
726
|
// it, and a durable decision already spoke for the call).
|
|
727
727
|
if (durable === undefined && chainVerdict === undefined && hooks.onPreTool) {
|
|
@@ -738,7 +738,7 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
738
738
|
return { action: "allow" };
|
|
739
739
|
}
|
|
740
740
|
/**
|
|
741
|
-
* The human approval pause (Phase D /
|
|
741
|
+
* The human approval pause (Phase D / ruling A): register the resolver
|
|
742
742
|
* BEFORE announcing the request (a consumer that answers the moment it
|
|
743
743
|
* sees the event must find the resolver already waiting — no deadlock
|
|
744
744
|
* between push and await), persist the request (via push), await the
|
|
@@ -767,7 +767,7 @@ async function humanPause(call, decisionId, hooks, log, resolveApproval, resolve
|
|
|
767
767
|
}
|
|
768
768
|
catch (err) {
|
|
769
769
|
if (err === ABORTED) {
|
|
770
|
-
//
|
|
770
|
+
// round 4 (adversarial): the human may have answered in the same instant
|
|
771
771
|
// the abort landed — a CONSUMED verdict must be recorded
|
|
772
772
|
// (exactly once), never lost; the abort then ends the run with
|
|
773
773
|
// its honest aborted terminal.
|
|
@@ -791,7 +791,7 @@ async function humanPause(call, decisionId, hooks, log, resolveApproval, resolve
|
|
|
791
791
|
push({
|
|
792
792
|
type: "permission_decided",
|
|
793
793
|
decisionId,
|
|
794
|
-
callId: call.callId, // binds the decision to the invocation (B
|
|
794
|
+
callId: call.callId, // binds the decision to the invocation (B group)
|
|
795
795
|
decision: finalDecision.action === "allow" ? "approved" : "denied",
|
|
796
796
|
...(finalDecision.action === "deny" && finalDecision.reason !== undefined
|
|
797
797
|
? { reason: finalDecision.reason }
|
|
@@ -808,7 +808,7 @@ async function humanPause(call, decisionId, hooks, log, resolveApproval, resolve
|
|
|
808
808
|
*/
|
|
809
809
|
async function runLedgered(call, registry, hooks, ctx, signal, push) {
|
|
810
810
|
const tool = registry.get(call.name); // validated + decided by decideCall
|
|
811
|
-
// C
|
|
811
|
+
// C group: the signal is re-checked immediately before the started event —
|
|
812
812
|
// an abort that landed in any permission path must not let the side
|
|
813
813
|
// effect begin.
|
|
814
814
|
if (signal?.aborted)
|
|
@@ -830,7 +830,7 @@ async function runLedgered(call, registry, hooks, ctx, signal, push) {
|
|
|
830
830
|
});
|
|
831
831
|
let result;
|
|
832
832
|
try {
|
|
833
|
-
// C
|
|
833
|
+
// C group: re-checked again right before the handler — the handler also
|
|
834
834
|
// observes ctx.signal, but the gate itself must not invoke it after
|
|
835
835
|
// a cancel.
|
|
836
836
|
result = signal?.aborted
|
|
@@ -847,7 +847,7 @@ async function runLedgered(call, registry, hooks, ctx, signal, push) {
|
|
|
847
847
|
if (hooks.onPostTool) {
|
|
848
848
|
result = await hooks.onPostTool({ callId: call.callId, name: call.name, input: call.input ?? {} }, result, ctx);
|
|
849
849
|
}
|
|
850
|
-
//
|
|
850
|
+
// ruling #12 correction one: a non-idempotent failure's side effects may have
|
|
851
851
|
// partially applied — an honest note rides the RESULT (and the failed
|
|
852
852
|
// receipt below, losslessly — a crash-window repair of the tool_result
|
|
853
853
|
// reproduces the normal path). Idempotent failures carry no note; the
|
|
@@ -863,7 +863,7 @@ async function runLedgered(call, registry, hooks, ctx, signal, push) {
|
|
|
863
863
|
// Area 3: only a tool that PROVED safe-to-retry (idempotent) gets a
|
|
864
864
|
// clean failure; a non-idempotent failure may have produced a side
|
|
865
865
|
// effect and is uncertain until a human decides.
|
|
866
|
-
//
|
|
866
|
+
// round 8: the tags ride on the RECEIPT too — a crash-window repair of the
|
|
867
867
|
// tool_result reproduces the normal path losslessly.
|
|
868
868
|
push({
|
|
869
869
|
type: "tool_execution_failed",
|
|
@@ -893,7 +893,7 @@ async function runLedgered(call, registry, hooks, ctx, signal, push) {
|
|
|
893
893
|
const ABORTED = Symbol("kiso-aborted-during-approval");
|
|
894
894
|
/**
|
|
895
895
|
* Wait for a human decision (approval or uncertain verdict), but WAKE on
|
|
896
|
-
* abort (Area 4 / C
|
|
896
|
+
* abort (Area 4 / C group): a cancel during the wait must end the run, not
|
|
897
897
|
* leave the iterator hung. Throws ABORTED; the loop converts it to an
|
|
898
898
|
* `aborted` terminal.
|
|
899
899
|
*/
|
|
@@ -956,14 +956,14 @@ function sleep(ms, signal) {
|
|
|
956
956
|
});
|
|
957
957
|
}
|
|
958
958
|
/**
|
|
959
|
-
* C
|
|
959
|
+
* C area (bootstrap #3): the DEFAULT for how many of the NEWEST compactable tool
|
|
960
960
|
* results survive a microcompact boundary (overridable per config via
|
|
961
961
|
* microcompact.keepResults) — the model must keep reasoning over the
|
|
962
962
|
* recent results, whatever turn they belong to.
|
|
963
963
|
*/
|
|
964
964
|
const KEEP_COMPACTABLE_RESULTS = 4;
|
|
965
965
|
/**
|
|
966
|
-
* C
|
|
966
|
+
* C area: the boundary seq for a microcompact — drawn by COMPACTABLE-RESULT
|
|
967
967
|
* recentness, never user turns: a SINGLE user turn that reads several big
|
|
968
968
|
* files (the coding agent's main overflow shape) crosses the threshold and
|
|
969
969
|
* must trigger. The newest `keepResults` still-visible compactable tool
|
|
@@ -987,11 +987,11 @@ function microcompactBoundarySeq(events, keepResults) {
|
|
|
987
987
|
if (ev.type !== "tool_result" || ev.seq <= lastCleared)
|
|
988
988
|
continue;
|
|
989
989
|
const name = callName.get(ev.callId);
|
|
990
|
-
//
|
|
990
|
+
// the ergonomics batch C6 (P4): the do-not-compact tag makes a result UN-CLEARABLE
|
|
991
991
|
// (the projection keeps it) — the count must EXCLUDE it, exactly like
|
|
992
992
|
// the clearing side. A tagged result counted here would steal a keep
|
|
993
993
|
// window slot forever and could anchor the boundary at a result the
|
|
994
|
-
// projection refuses to clear (
|
|
994
|
+
// projection refuses to clear (the count and the clearing share one rule).
|
|
995
995
|
if (name !== undefined && MICROCOMPACTABLE.has(name) && !(ev.tags ?? []).includes(DO_NOT_COMPACT))
|
|
996
996
|
visible.push(ev.seq);
|
|
997
997
|
}
|
package/dist/kernel/project.d.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* Events with no message shape (usage, stop, thinking, terminal, the
|
|
17
17
|
* compaction events' own records) are skipped by the projection;
|
|
18
18
|
* `compacted` applies the EXACT persisted replacements verbatim — it never
|
|
19
|
-
* re-runs the compaction algorithm (a future version could differ, A
|
|
20
|
-
*
|
|
19
|
+
* re-runs the compaction algorithm (a future version could differ, A group/D
|
|
20
|
+
* group); `microcompacted` boundaries re-derive the cleared view from the
|
|
21
21
|
* stream itself (deterministic and idempotent); `summarized` (ADR-0044)
|
|
22
22
|
* replaces its covered range with one assistant summary message. All
|
|
23
23
|
* three are persisted facts — the replay equals the live run. See ADR-0002.
|
|
@@ -26,18 +26,18 @@ import type { Event } from "../protocol/events.js";
|
|
|
26
26
|
import type { EventInput } from "./event-log.js";
|
|
27
27
|
import type { AssistantBlock, Message, MessageSource } from "../protocol/messages.js";
|
|
28
28
|
/**
|
|
29
|
-
* C
|
|
29
|
+
* C area: tools whose output is eligible for microcompact clearing — reads,
|
|
30
30
|
* listings, searches, and shell output. write/edit outputs are short and
|
|
31
|
-
* never cleared. Exported so the loop's boundary computation (
|
|
31
|
+
* never cleared. Exported so the loop's boundary computation (bootstrap #3)
|
|
32
32
|
* counts exactly the results the projection can clear.
|
|
33
33
|
*/
|
|
34
34
|
export declare const MICROCOMPACTABLE: Set<string>;
|
|
35
|
-
/** The tag that makes a tool result un-clearable (C
|
|
35
|
+
/** The tag that makes a tool result un-clearable (C area). */
|
|
36
36
|
export declare const DO_NOT_COMPACT = "do-not-compact";
|
|
37
37
|
/**
|
|
38
38
|
* Rebuild the message array from events. Deterministic and order-sensitive:
|
|
39
39
|
* replaying the same log always produces the same messages — BYTE FOR BYTE
|
|
40
|
-
* (D
|
|
40
|
+
* (D area): the same event prefix derives the same message prefix; the only
|
|
41
41
|
* events that change already-derived messages are `microcompacted`
|
|
42
42
|
* boundaries and `summarized` facts, which are themselves persisted facts
|
|
43
43
|
* (their replay derives the same projection every time).
|
package/dist/kernel/project.js
CHANGED
|
@@ -16,23 +16,23 @@
|
|
|
16
16
|
* Events with no message shape (usage, stop, thinking, terminal, the
|
|
17
17
|
* compaction events' own records) are skipped by the projection;
|
|
18
18
|
* `compacted` applies the EXACT persisted replacements verbatim — it never
|
|
19
|
-
* re-runs the compaction algorithm (a future version could differ, A
|
|
20
|
-
*
|
|
19
|
+
* re-runs the compaction algorithm (a future version could differ, A group/D
|
|
20
|
+
* group); `microcompacted` boundaries re-derive the cleared view from the
|
|
21
21
|
* stream itself (deterministic and idempotent); `summarized` (ADR-0044)
|
|
22
22
|
* replaces its covered range with one assistant summary message. All
|
|
23
23
|
* three are persisted facts — the replay equals the live run. See ADR-0002.
|
|
24
24
|
*/
|
|
25
25
|
/**
|
|
26
|
-
* C
|
|
26
|
+
* C area: tools whose output is eligible for microcompact clearing — reads,
|
|
27
27
|
* listings, searches, and shell output. write/edit outputs are short and
|
|
28
|
-
* never cleared. Exported so the loop's boundary computation (
|
|
28
|
+
* never cleared. Exported so the loop's boundary computation (bootstrap #3)
|
|
29
29
|
* counts exactly the results the projection can clear.
|
|
30
30
|
*/
|
|
31
31
|
export const MICROCOMPACTABLE = new Set(["read_file", "list_dir", "search_text", "shell"]);
|
|
32
|
-
/** The tag that makes a tool result un-clearable (C
|
|
32
|
+
/** The tag that makes a tool result un-clearable (C area). */
|
|
33
33
|
export const DO_NOT_COMPACT = "do-not-compact";
|
|
34
34
|
/**
|
|
35
|
-
* C
|
|
35
|
+
* C area: the fixed placeholder for a cleared tool output, derived ONLY from
|
|
36
36
|
* the event stream (deterministic across replay): the tool's name and its
|
|
37
37
|
* primary argument (the first string-valued field of its input).
|
|
38
38
|
*/
|
|
@@ -54,7 +54,7 @@ function primaryArg(input) {
|
|
|
54
54
|
/**
|
|
55
55
|
* Rebuild the message array from events. Deterministic and order-sensitive:
|
|
56
56
|
* replaying the same log always produces the same messages — BYTE FOR BYTE
|
|
57
|
-
* (D
|
|
57
|
+
* (D area): the same event prefix derives the same message prefix; the only
|
|
58
58
|
* events that change already-derived messages are `microcompacted`
|
|
59
59
|
* boundaries and `summarized` facts, which are themselves persisted facts
|
|
60
60
|
* (their replay derives the same projection every time).
|
|
@@ -75,9 +75,9 @@ export function projectMessages(events) {
|
|
|
75
75
|
text = null;
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
|
-
//
|
|
78
|
+
// bootstrap P1: the reasoning of the turn being built, accumulated from its
|
|
79
79
|
// `thinking` events and attached to the assistant message at flush —
|
|
80
|
-
// deterministic (same events → same messages → same request body, D
|
|
80
|
+
// deterministic (same events → same messages → same request body, D area).
|
|
81
81
|
let pendingReasoning = null;
|
|
82
82
|
const flushAssistant = () => {
|
|
83
83
|
pushText();
|
|
@@ -96,7 +96,7 @@ export function projectMessages(events) {
|
|
|
96
96
|
blocks = [];
|
|
97
97
|
assistantSource = undefined;
|
|
98
98
|
};
|
|
99
|
-
// C
|
|
99
|
+
// C group/round 6: vetoed/rewritten user inputs. Collect the replacement map
|
|
100
100
|
// first — the FINAL replacement for each input wins (later replacements
|
|
101
101
|
// never produce extra messages), and the replacement renders AT THE
|
|
102
102
|
// INPUT'S OWN POSITION: the original is skipped, the final non-null
|
|
@@ -111,7 +111,7 @@ export function projectMessages(events) {
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
// C
|
|
114
|
+
// C area: callId → {name, input} for the microcompact placeholder.
|
|
115
115
|
const callMeta = new Map();
|
|
116
116
|
for (const ev of events) {
|
|
117
117
|
if (ev.type === "tool_call_end")
|
|
@@ -144,8 +144,8 @@ export function projectMessages(events) {
|
|
|
144
144
|
// physical seq order is the COMPLETION order (started/receipt/result land
|
|
145
145
|
// when each execution finishes — parallel), which must never enter the
|
|
146
146
|
// projection: the same logical turn projects byte-identically whatever
|
|
147
|
-
// the completion interleaving (
|
|
148
|
-
//
|
|
147
|
+
// the completion interleaving (the byte discipline — call order is the truth; completion order only affects
|
|
148
|
+
// the moment of writing). `callOrder` is rebuilt per turn from the tool_call_end
|
|
149
149
|
// events (their seq order IS the call order — the stream order).
|
|
150
150
|
let resultBuf = [];
|
|
151
151
|
const callOrder = new Map();
|
|
@@ -188,7 +188,7 @@ export function projectMessages(events) {
|
|
|
188
188
|
// first, then its results in call order (the turn boundary).
|
|
189
189
|
flushAssistant();
|
|
190
190
|
flushResults();
|
|
191
|
-
//
|
|
191
|
+
// round 6: the final replacement renders HERE, at the input's own
|
|
192
192
|
// position — the original is skipped, the replacement event
|
|
193
193
|
// itself produces nothing (a later replacement for the same
|
|
194
194
|
// input never becomes a second message).
|
|
@@ -214,10 +214,10 @@ export function projectMessages(events) {
|
|
|
214
214
|
}
|
|
215
215
|
case "user_input_replaced":
|
|
216
216
|
// The replacement already rendered at its input's position —
|
|
217
|
-
// this event carries no message of its own (
|
|
217
|
+
// this event carries no message of its own (round 6).
|
|
218
218
|
break;
|
|
219
219
|
case "assistant_start":
|
|
220
|
-
// D
|
|
220
|
+
// D group: an explicit message boundary — close any open message
|
|
221
221
|
// and begin a new one (adjacent assistants stay separate).
|
|
222
222
|
flushAssistant();
|
|
223
223
|
flushResults();
|
|
@@ -304,7 +304,7 @@ export function projectMessages(events) {
|
|
|
304
304
|
...(ev.source !== undefined ? { source: ev.source } : {}),
|
|
305
305
|
...(ev.tags !== undefined ? { tags: ev.tags } : {}),
|
|
306
306
|
};
|
|
307
|
-
//
|
|
307
|
+
// round 5: the originating event's seq rides on the message as a
|
|
308
308
|
// NON-ENUMERABLE correlation field — the stable identity
|
|
309
309
|
// compaction uses to name WHICH result it replaced. Deep
|
|
310
310
|
// equality with seed messages (which carry no such field)
|
|
@@ -322,7 +322,7 @@ export function projectMessages(events) {
|
|
|
322
322
|
flushAssistant();
|
|
323
323
|
flushResults(); // the results must be in `out` before the replacement pass
|
|
324
324
|
flushAssistant();
|
|
325
|
-
// C
|
|
325
|
+
// C area: replace every eligible OLD tool result with the fixed
|
|
326
326
|
// placeholder. Eligibility: the result's own event seq <= the
|
|
327
327
|
// boundary, its tool in the whitelist, and no do-not-compact
|
|
328
328
|
// tag. Deterministic — the boundary event IS the decision.
|
|
@@ -346,10 +346,10 @@ export function projectMessages(events) {
|
|
|
346
346
|
flushResults(); // the results must be in `out` before the replacement pass
|
|
347
347
|
flushAssistant();
|
|
348
348
|
// Apply the EXACT persisted replacements — never re-run the
|
|
349
|
-
// compaction algorithm (a future version could differ).
|
|
349
|
+
// compaction algorithm (a future version could differ). round 5:
|
|
350
350
|
// v2 entries are keyed by the replaced tool-result EVENT's
|
|
351
351
|
// seq, so only the specific result is rewritten — never a
|
|
352
|
-
// same-callId sibling from another turn or run.
|
|
352
|
+
// same-callId sibling from another turn or run. round 4: v1
|
|
353
353
|
// entries (round-three sessions, no eventSeq) replay with v1
|
|
354
354
|
// semantics — every tool result with that callId is replaced,
|
|
355
355
|
// exactly as the old framework did.
|
|
@@ -369,7 +369,7 @@ export function projectMessages(events) {
|
|
|
369
369
|
break;
|
|
370
370
|
}
|
|
371
371
|
case "thinking":
|
|
372
|
-
//
|
|
372
|
+
// bootstrap P1: accumulate the turn's reasoning — the flush (an
|
|
373
373
|
// empty one at the turn's start) keeps the pending text, and
|
|
374
374
|
// the assistant message that follows carries it. 0.1.26: the
|
|
375
375
|
// assistant flush is guarded on the buffered results (a
|
|
@@ -440,9 +440,9 @@ export function messagesToEvents(messages) {
|
|
|
440
440
|
break;
|
|
441
441
|
}
|
|
442
442
|
case "assistant": {
|
|
443
|
-
// D
|
|
443
|
+
// D group: an explicit assistant_start/assistant_end pair frames
|
|
444
444
|
// the message — adjacent and empty assistants round-trip.
|
|
445
|
-
//
|
|
445
|
+
// bootstrap P1: the reasoning re-enters the log as its ORIGINAL
|
|
446
446
|
// thinking event — the projection re-attaches it identically.
|
|
447
447
|
if (msg.reasoning !== undefined) {
|
|
448
448
|
out.push({ type: "thinking", text: msg.reasoning });
|
|
@@ -49,7 +49,7 @@ export interface StreamOptions {
|
|
|
49
49
|
readonly signal?: AbortSignalLike;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* The NARROW event set an adapter may produce (
|
|
52
|
+
* The NARROW event set an adapter may produce (round 5). Everything else in the
|
|
53
53
|
* union is kernel-owned — `terminal`, `tool_execution_*`, `permission_*`,
|
|
54
54
|
* `user_input`, `compacted`, `summarized`, `uncertain_pending`,
|
|
55
55
|
* `user_input_replaced`, `assistant_start`/`assistant_end` — and a
|
|
@@ -63,7 +63,7 @@ export type AdapterEvent = Extract<Event, {
|
|
|
63
63
|
/** Runtime whitelist backing the narrowed type — the loop's trust gate. */
|
|
64
64
|
export declare const ADAPTER_EVENT_TYPES: ReadonlySet<string>;
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
66
|
+
* round 5(P1-8): the trust gate validates STRUCTURE, not just the type name.
|
|
67
67
|
* A third-party adapter can emit a legal type with illegal fields (a stop
|
|
68
68
|
* without a reason, a usage with known:true and no token, an array tool
|
|
69
69
|
* input) — persisted, that would poison the next load. The gate reuses the
|
package/dist/protocol/adapter.js
CHANGED
|
@@ -31,7 +31,7 @@ export const ADAPTER_EVENT_TYPES = new Set([
|
|
|
31
31
|
"stop",
|
|
32
32
|
]);
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* round 5(P1-8): the trust gate validates STRUCTURE, not just the type name.
|
|
35
35
|
* A third-party adapter can emit a legal type with illegal fields (a stop
|
|
36
36
|
* without a reason, a usage with known:true and no token, an array tool
|
|
37
37
|
* input) — persisted, that would poison the next load. The gate reuses the
|
|
@@ -62,7 +62,7 @@ export interface TextStart {
|
|
|
62
62
|
readonly source?: import("./messages.js").MessageSource;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* The explicit boundary of an assistant message (D
|
|
65
|
+
* The explicit boundary of an assistant message (D group). Adapters never emit
|
|
66
66
|
* these (their implicit boundaries — tool_result/user_input/terminal — are
|
|
67
67
|
* enough); the seed encoder uses them so ADJACENT assistant messages and
|
|
68
68
|
* EMPTY assistant messages round-trip losslessly.
|
|
@@ -134,12 +134,12 @@ export interface ToolResultEvent {
|
|
|
134
134
|
readonly seq: number;
|
|
135
135
|
readonly type: "tool_result";
|
|
136
136
|
readonly callId: string;
|
|
137
|
-
/** Full content — blocks preserved losslessly (D
|
|
137
|
+
/** Full content — blocks preserved losslessly (D group). */
|
|
138
138
|
readonly content: string | readonly import("./messages.js").ContentBlock[];
|
|
139
139
|
readonly isError: boolean;
|
|
140
140
|
/** Present only when `isError` is true and the handler classified it. */
|
|
141
141
|
readonly errorKind?: ToolErrorKind;
|
|
142
|
-
/** The execution that produced this result (B
|
|
142
|
+
/** The execution that produced this result (B group) — receipt pairing key. */
|
|
143
143
|
readonly executionId?: string;
|
|
144
144
|
/** Provenance + product tags — preserved losslessly (Area 6). */
|
|
145
145
|
readonly source?: import("./messages.js").MessageSource;
|
|
@@ -161,17 +161,17 @@ export interface UserInputEvent {
|
|
|
161
161
|
/**
|
|
162
162
|
* Compaction happened at this point in the trajectory. The EXACT
|
|
163
163
|
* replacements are persisted; the projection applies them verbatim — it
|
|
164
|
-
* never re-runs a future version of the compaction algorithm (A
|
|
164
|
+
* never re-runs a future version of the compaction algorithm (A group/D group).
|
|
165
165
|
* The replay therefore equals the live run byte for byte, independent of
|
|
166
166
|
* algorithm drift.
|
|
167
167
|
*
|
|
168
|
-
*
|
|
168
|
+
* round 5: `eventSeq` is the STABLE identity — the seq of the specific
|
|
169
169
|
* `tool_result` event that was replaced. The provider callId may repeat
|
|
170
170
|
* across runs and is correlation-only; `callId` is kept for traceability.
|
|
171
171
|
* Only THIS turn's NEWLY cleared results are listed, never a cumulative
|
|
172
|
-
* set of already-cleared markers (
|
|
172
|
+
* set of already-cleared markers (round 5).
|
|
173
173
|
*
|
|
174
|
-
*
|
|
174
|
+
* round 4: `eventSeq` is OPTIONAL because sessions written by round three
|
|
175
175
|
* (v1) carry `{callId, content}` entries without it. Those are legal and
|
|
176
176
|
* replay with v1 semantics (replace every tool result with that callId,
|
|
177
177
|
* exactly as the old framework did); records written from now on always
|
|
@@ -212,7 +212,7 @@ export interface ToolExecutionSucceeded {
|
|
|
212
212
|
readonly content: string;
|
|
213
213
|
readonly isError: false;
|
|
214
214
|
};
|
|
215
|
-
/**
|
|
215
|
+
/** round 8: the tags ride on the durable RECEIPT so a crash-window repair
|
|
216
216
|
* of the tool_result can reproduce the normal path losslessly. */
|
|
217
217
|
readonly tags?: readonly string[];
|
|
218
218
|
}
|
|
@@ -230,7 +230,7 @@ export interface ToolExecutionFailed {
|
|
|
230
230
|
readonly error: string;
|
|
231
231
|
readonly errorKind?: ToolErrorKind;
|
|
232
232
|
readonly safeToRetry: boolean;
|
|
233
|
-
/**
|
|
233
|
+
/** round 8: tags on the durable receipt, preserved across crash-window repair. */
|
|
234
234
|
readonly tags?: readonly string[];
|
|
235
235
|
}
|
|
236
236
|
/**
|
|
@@ -264,7 +264,7 @@ export interface PermissionDecided {
|
|
|
264
264
|
readonly seq: number;
|
|
265
265
|
readonly type: "permission_decided";
|
|
266
266
|
readonly decisionId: string;
|
|
267
|
-
/** The invocation this decision binds to (B
|
|
267
|
+
/** The invocation this decision binds to (B group). */
|
|
268
268
|
readonly callId?: string;
|
|
269
269
|
readonly decision: "approved" | "denied";
|
|
270
270
|
readonly reason?: string;
|
|
@@ -273,7 +273,7 @@ export interface PermissionDecided {
|
|
|
273
273
|
readonly decidedBy?: string;
|
|
274
274
|
}
|
|
275
275
|
/**
|
|
276
|
-
* A permission request was CLOSED because its run terminated first (B
|
|
276
|
+
* A permission request was CLOSED because its run terminated first (B group):
|
|
277
277
|
* an aborted/completed/error run's dangling approval is dead — it is never
|
|
278
278
|
* re-presented and a late approve() cannot resurrect the run.
|
|
279
279
|
*/
|
|
@@ -285,7 +285,7 @@ export interface PermissionExpired {
|
|
|
285
285
|
}
|
|
286
286
|
/**
|
|
287
287
|
* A non-idempotent execution FAILED and the run PAUSES until a human
|
|
288
|
-
* decides (C
|
|
288
|
+
* decides (C group): no next model turn, no sibling tool, no auto-retry. The
|
|
289
289
|
* verdict is recorded by the session (resolveUncertain) and the ledger
|
|
290
290
|
* transitions uncertain → rerun/abandoned; the event itself is the durable
|
|
291
291
|
* pause marker.
|
|
@@ -299,7 +299,7 @@ export interface UncertainPending {
|
|
|
299
299
|
readonly error: string;
|
|
300
300
|
}
|
|
301
301
|
/**
|
|
302
|
-
* A user input was VETOED or REWRITTEN by the harness (C
|
|
302
|
+
* A user input was VETOED or REWRITTEN by the harness (C group). `replaces`
|
|
303
303
|
* is the seq of the original user_input; the projection skips the original
|
|
304
304
|
* and, when `content` is non-null, produces the replacement instead — the
|
|
305
305
|
* rewritten fact is the ONLY fact every later turn sees. null content = a
|
|
@@ -310,11 +310,11 @@ export interface UserInputReplaced {
|
|
|
310
310
|
readonly type: "user_input_replaced";
|
|
311
311
|
readonly replaces: number;
|
|
312
312
|
readonly content: string | readonly import("./messages.js").ContentBlock[] | null;
|
|
313
|
-
/** Provenance of the replacement — preserved from the hook (
|
|
313
|
+
/** Provenance of the replacement — preserved from the hook (round 3). */
|
|
314
314
|
readonly source?: import("./messages.js").MessageSource;
|
|
315
315
|
}
|
|
316
316
|
/**
|
|
317
|
-
* C
|
|
317
|
+
* C area: a MICROCOMPACT boundary — the durable record of a context-clearing
|
|
318
318
|
* decision. `beforeSeq` is the event seq up to which eligible tool results
|
|
319
319
|
* are cleared: the projection replaces every tool_result with seq <=
|
|
320
320
|
* beforeSeq (whose tool is in the compactable whitelist and carries no
|
package/dist/protocol/events.js
CHANGED
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
* This module is almost types-only: the only runtime value it emits is
|
|
23
23
|
* `isKisoEvent`, the type guard the session store validates records with.
|
|
24
24
|
*/
|
|
25
|
-
// ── deep-shape helpers (
|
|
25
|
+
// ── deep-shape helpers (round 5): every variant is validated field by field —
|
|
26
26
|
// legal enums, Terminal union members, Usage known/token combos, content
|
|
27
27
|
// blocks, plain-object inputs, optional fields when present. A record that
|
|
28
28
|
// parses as JSON but violates its variant's shape is corruption, never
|
|
29
29
|
// history.
|
|
30
30
|
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
31
|
-
/**
|
|
31
|
+
/** round 9: counts (seq, tokens, turns, status) are non-negative SAFE integers —
|
|
32
32
|
* negative values, NaN, Infinity, and fractional values are rejected. */
|
|
33
33
|
const isNonNegativeInt = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
34
34
|
const STOP_REASONS = new Set([
|
|
@@ -68,7 +68,7 @@ const MEDIA_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif
|
|
|
68
68
|
/**
|
|
69
69
|
* A ContentBlock: text (text: string) or image (sourceType with the
|
|
70
70
|
* documented payload — url for "url", data + mediaType for "base64").
|
|
71
|
-
*
|
|
71
|
+
* round 9: the two payload kinds are STRICTLY EXCLUSIVE — a url block must not
|
|
72
72
|
* carry data, a base64 block must not carry url.
|
|
73
73
|
*/
|
|
74
74
|
function isContentBlock(v) {
|
|
@@ -102,7 +102,7 @@ function isErrorKind(v) {
|
|
|
102
102
|
function isExecutionId(v) {
|
|
103
103
|
return v.executionId === undefined || typeof v.executionId === "string";
|
|
104
104
|
}
|
|
105
|
-
/** A StructuredError — the shape the `error` terminal carries.
|
|
105
|
+
/** A StructuredError — the shape the `error` terminal carries. round 9: a
|
|
106
106
|
* status, when present, is a non-negative safe integer (no negatives,
|
|
107
107
|
* NaN, Infinity, or fractions). */
|
|
108
108
|
function isStructuredError(v) {
|
|
@@ -122,7 +122,7 @@ function isTerminal(v) {
|
|
|
122
122
|
case "max_tokens":
|
|
123
123
|
return true;
|
|
124
124
|
case "max_turns":
|
|
125
|
-
return isNonNegativeInt(v.turns); //
|
|
125
|
+
return isNonNegativeInt(v.turns); // round 9: a turn count is a safe integer
|
|
126
126
|
case "error":
|
|
127
127
|
return isStructuredError(v.error);
|
|
128
128
|
case "aborted":
|
|
@@ -134,7 +134,7 @@ function isTerminal(v) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
|
-
* Usage invariant (Area 6
|
|
137
|
+
* Usage invariant (Area 6/round 9): `known: false` means the provider reported
|
|
138
138
|
* NO usage — every token field is null, never faked as zero. `known: true`
|
|
139
139
|
* means SOME usage was reported — AT LEAST ONE field is a non-negative
|
|
140
140
|
* safe integer; the others are null when the provider did not report
|
|
@@ -149,7 +149,7 @@ function isUsage(v) {
|
|
|
149
149
|
return tokens.some((t) => isNonNegativeInt(t)) && tokens.every((t) => t === null || isNonNegativeInt(t));
|
|
150
150
|
}
|
|
151
151
|
/**
|
|
152
|
-
* Per-variant runtime validation (
|
|
152
|
+
* Per-variant runtime validation (round 5): the table is
|
|
153
153
|
* `satisfies Record<Event["type"], ...>` — adding a variant without a
|
|
154
154
|
* validator here is a compile error (ADR-0003).
|
|
155
155
|
*/
|
|
@@ -165,7 +165,7 @@ const EVENT_VALIDATORS = {
|
|
|
165
165
|
tool_result: (v) => typeof v.callId === "string" &&
|
|
166
166
|
isContent(v.content) &&
|
|
167
167
|
typeof v.isError === "boolean" &&
|
|
168
|
-
//
|
|
168
|
+
// round 9: an errorKind is only meaningful on an ERROR result.
|
|
169
169
|
(v.isError === true || v.errorKind === undefined) &&
|
|
170
170
|
isErrorKind(v) &&
|
|
171
171
|
isExecutionId(v) &&
|
|
@@ -177,7 +177,7 @@ const EVENT_VALIDATORS = {
|
|
|
177
177
|
user_input: (v) => isContent(v.content) && isSource(v),
|
|
178
178
|
compacted: (v) => Array.isArray(v.cleared) &&
|
|
179
179
|
v.cleared.every((c) => isPlainObject(c) &&
|
|
180
|
-
//
|
|
180
|
+
// round 4: eventSeq is optional — v1 (round three) entries are
|
|
181
181
|
// {callId, content} and remain legal; v2 entries must carry a
|
|
182
182
|
// valid eventSeq.
|
|
183
183
|
(c.eventSeq === undefined || isNonNegativeInt(c.eventSeq)) &&
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* E1 — extension approval policies: pure types, no runtime.
|
|
3
3
|
*
|
|
4
4
|
* An extension is a named bundle of optional capabilities: hooks (composed
|
|
5
|
-
* AFTER the harness's own —
|
|
5
|
+
* AFTER the harness's own — the existing come first), tools (merged into the registry), and
|
|
6
6
|
* approval policies (the loop's policy chain, decided BEFORE the human
|
|
7
7
|
* flow). This file is types-only: loading and composition live in the
|
|
8
8
|
* runtime package (loadExtensions) and the kernel loop.
|
|
@@ -68,13 +68,13 @@ export interface KisoExtension {
|
|
|
68
68
|
readonly append: string;
|
|
69
69
|
};
|
|
70
70
|
/**
|
|
71
|
-
* 0.1.26 (MCP
|
|
71
|
+
* 0.1.26 (MCP lazy connection): an optional LIVE flag — the CLI's banner renders
|
|
72
72
|
* "name (connecting…)" while it is true. Soft surface: absent = no
|
|
73
73
|
* marker (the default).
|
|
74
74
|
*/
|
|
75
75
|
readonly connecting?: boolean;
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
77
|
+
* finding #8 (P1): the extension's shutdown action — the closing of external
|
|
78
78
|
* resources it holds (child processes, connections). The LOADER is
|
|
79
79
|
* responsible for calling it.
|
|
80
80
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* E1 — extension approval policies: pure types, no runtime.
|
|
3
3
|
*
|
|
4
4
|
* An extension is a named bundle of optional capabilities: hooks (composed
|
|
5
|
-
* AFTER the harness's own —
|
|
5
|
+
* AFTER the harness's own — the existing come first), tools (merged into the registry), and
|
|
6
6
|
* approval policies (the loop's policy chain, decided BEFORE the human
|
|
7
7
|
* flow). This file is types-only: loading and composition live in the
|
|
8
8
|
* runtime package (loadExtensions) and the kernel loop.
|
|
@@ -72,7 +72,7 @@ export interface AssistantMessage {
|
|
|
72
72
|
readonly blocks: readonly AssistantBlock[];
|
|
73
73
|
readonly source?: MessageSource;
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* bootstrap P1: the turn's reasoning, derived deterministically from its
|
|
76
76
|
* `thinking` events (DeepSeek's thinking mode requires it back on
|
|
77
77
|
* follow-up requests). Present only when the turn actually reasoned.
|
|
78
78
|
*/
|
|
@@ -96,7 +96,7 @@ export interface ToolResultMessage {
|
|
|
96
96
|
/**
|
|
97
97
|
* The seq of the `tool_result` EVENT this message was projected from —
|
|
98
98
|
* the stable identity compaction uses to name WHICH result it replaced
|
|
99
|
-
* (
|
|
99
|
+
* (round 5: the callId may repeat across runs). DERIVED, never persisted: the
|
|
100
100
|
* projection attaches it as a non-enumerable field, so it is invisible
|
|
101
101
|
* to deep equality with seed messages and never survives a re-derive
|
|
102
102
|
* from anywhere but the log.
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* model cannot call a tool that is not in its registry — no prompt can
|
|
13
13
|
* achieve that guarantee.
|
|
14
14
|
*
|
|
15
|
-
* 0.1.26 (MCP
|
|
15
|
+
* 0.1.26 (MCP lazy connection): `registerLive()` adds a LIVE tool source — a
|
|
16
16
|
* function returning the extension's current tools array. The array grows
|
|
17
17
|
* when the extension's background connections settle (the MCP bridge
|
|
18
18
|
* registers its servers' tools post-connect); the registry consults the
|
package/dist/tools/registry.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* model cannot call a tool that is not in its registry — no prompt can
|
|
13
13
|
* achieve that guarantee.
|
|
14
14
|
*
|
|
15
|
-
* 0.1.26 (MCP
|
|
15
|
+
* 0.1.26 (MCP lazy connection): `registerLive()` adds a LIVE tool source — a
|
|
16
16
|
* function returning the extension's current tools array. The array grows
|
|
17
17
|
* when the extension's background connections settle (the MCP bridge
|
|
18
18
|
* registers its servers' tools post-connect); the registry consults the
|
|
@@ -49,7 +49,7 @@ export class ToolRegistry {
|
|
|
49
49
|
list() {
|
|
50
50
|
// The registered map WINS a name collision against a live source —
|
|
51
51
|
// the same rule get()/has() already follow, applied here too
|
|
52
|
-
// (0.1.27
|
|
52
|
+
// (0.1.27 the disqualification investigation: the agent eagerly registers a sync extension's
|
|
53
53
|
// tools AND registers its live source — a skills/subagent/MCP status
|
|
54
54
|
// tool appeared twice in toSpecs() and the real API answered
|
|
55
55
|
// "400 Tool names must be unique"; the identity is the same object,
|
package/dist/tools/tool.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface ToolContext {
|
|
|
27
27
|
readonly meta?: Readonly<Record<string, unknown>>;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* round 5(P1-9): a DISCRIMINATED union — `errorKind` is structurally
|
|
31
31
|
* impossible on a non-error result, matching the persisted-event schema
|
|
32
32
|
* (an isError:false result with an errorKind would be rejected by the
|
|
33
33
|
* store's validator and poison the next load).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "kiso(
|
|
3
|
+
"version": "0.1.30",
|
|
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",
|
|
7
7
|
"exports": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"openai"
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@vincemakes/kiso-evals": "0.1.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.1.29",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|