@vincemakes/kiso-core 0.15.11 → 0.16.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/kernel/loop.d.ts +14 -1
- package/dist/kernel/loop.js +113 -69
- package/dist/kernel/project.js +20 -4
- package/dist/protocol/adapter.d.ts +7 -0
- package/dist/protocol/events.d.ts +33 -0
- package/dist/protocol/events.js +28 -1
- package/dist/protocol/messages.d.ts +7 -0
- package/package.json +2 -2
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* re-stream that duplicates output or tool calls.
|
|
34
34
|
*/
|
|
35
35
|
import { type Adapter, type AbortSignalLike } from "../protocol/adapter.js";
|
|
36
|
-
import type { Event, StructuredError } from "../protocol/events.js";
|
|
36
|
+
import type { ContinuationScope, Event, StructuredError } from "../protocol/events.js";
|
|
37
37
|
import type { ApprovalChain } from "../protocol/extension.js";
|
|
38
38
|
import { EventLog } from "./event-log.js";
|
|
39
39
|
import type { EventInput } from "./event-log.js";
|
|
@@ -53,6 +53,19 @@ export interface LoopConfig {
|
|
|
53
53
|
readonly mode?: string;
|
|
54
54
|
readonly maxTurns?: number;
|
|
55
55
|
readonly maxRetries?: number;
|
|
56
|
+
/**
|
|
57
|
+
* MG-1 (ADR-0051 Amendment 5): the run's continuation scope — the
|
|
58
|
+
* kernel stamps it onto a committed stop's envelope (adapters cannot
|
|
59
|
+
* forge scope). Absent = an unscoped run (SDK-injected or faux
|
|
60
|
+
* adapters): adapter-emitted continuation is STRIPPED at the commit.
|
|
61
|
+
*/
|
|
62
|
+
readonly continuationScope?: ContinuationScope;
|
|
63
|
+
/** XP-1: the RESOLVED reasoning wire values — passed to the adapter
|
|
64
|
+
* verbatim; absent = provider defaults (the byte anchor). */
|
|
65
|
+
readonly reasoning?: {
|
|
66
|
+
readonly thinking?: "adaptive" | "enabled" | "disabled";
|
|
67
|
+
readonly effort?: string;
|
|
68
|
+
};
|
|
56
69
|
/**
|
|
57
70
|
* Seed history. When a `log` is provided, the log IS the truth and this
|
|
58
71
|
* is only used if the log is empty. See ADR-0002 / kernel/project.ts.
|
package/dist/kernel/loop.js
CHANGED
|
@@ -493,6 +493,29 @@ export async function* loop(config) {
|
|
|
493
493
|
turnSettled = new Promise((res) => {
|
|
494
494
|
settleTurn = res;
|
|
495
495
|
});
|
|
496
|
+
// F4b: the ONE abandon sequence, for EVERY uncommitted exit — the
|
|
497
|
+
// mid-stream error (retryable or not), the user abort, and the
|
|
498
|
+
// voided turn. Settle (parked launches bail on the un-committed
|
|
499
|
+
// gate), drain (started executions land their receipts), then
|
|
500
|
+
// durably void the draft. The defensive check stands: a started
|
|
501
|
+
// commit-required execution in the draft (impossible under
|
|
502
|
+
// invariant 3) suppresses the marker — no void over a started
|
|
503
|
+
// fact, the pre-F4 abandon exactly.
|
|
504
|
+
const unsafeStartedInDraft = () => log.all.some((e) => e.type === "tool_execution_started" && e.seq > turnStart && registry.get(e.name)?.effects?.precommitSafe !== true);
|
|
505
|
+
const abandonDraft = async function* (reason) {
|
|
506
|
+
settleTurn();
|
|
507
|
+
violated = true;
|
|
508
|
+
violatedReject();
|
|
509
|
+
yield* drainSettled();
|
|
510
|
+
if (launchError !== null)
|
|
511
|
+
throw launchError;
|
|
512
|
+
if (log.lastSeq > turnStart && !unsafeStartedInDraft()) {
|
|
513
|
+
const marker = log.append({ type: "model_output_abandoned", voidFromSeq: turnStart, reason });
|
|
514
|
+
if (hooks.onEvent)
|
|
515
|
+
await hooks.onEvent(marker, {}).catch(() => { });
|
|
516
|
+
yield marker;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
496
519
|
while (true) {
|
|
497
520
|
// Area 4: the backoff is abortable — a cancel landing during a
|
|
498
521
|
// retry wait ends the run now, not after the backoff.
|
|
@@ -504,6 +527,7 @@ export async function* loop(config) {
|
|
|
504
527
|
const stream = config.adapter.stream({
|
|
505
528
|
model: config.model,
|
|
506
529
|
messages,
|
|
530
|
+
...(config.reasoning !== undefined ? { reasoning: config.reasoning } : {}),
|
|
507
531
|
...(config.systemPrompt !== undefined ? { systemPrompt: config.systemPrompt } : {}),
|
|
508
532
|
tools: registry.toSpecs(),
|
|
509
533
|
...(config.maxTokens !== undefined ? { maxTokens: config.maxTokens } : {}),
|
|
@@ -577,8 +601,12 @@ export async function* loop(config) {
|
|
|
577
601
|
catch (err) {
|
|
578
602
|
// Area 4: a user cancel surfaced by the SDK (APIUserAbortError
|
|
579
603
|
// or any error while the signal is set) is an honest `aborted`
|
|
580
|
-
// terminal, never a generic error.
|
|
604
|
+
// terminal, never a generic error. F4b: the aborted draft is
|
|
605
|
+
// voided LIVE — pre-F4b only a resume voided it, so the same
|
|
606
|
+
// durable prefix projected differently depending on whether
|
|
607
|
+
// the process crashed first.
|
|
581
608
|
if (aborted()) {
|
|
609
|
+
yield* abandonDraft("the run was aborted before this turn committed");
|
|
582
610
|
yield await terminal({ kind: "aborted", by: "user" });
|
|
583
611
|
return;
|
|
584
612
|
}
|
|
@@ -591,42 +619,17 @@ export async function* loop(config) {
|
|
|
591
619
|
await sleep(attempts * 250, signal); // abortable backoff
|
|
592
620
|
continue;
|
|
593
621
|
}
|
|
594
|
-
// F4 — ABANDON HYGIENE, every streamed exit:
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
violated = true;
|
|
603
|
-
violatedReject();
|
|
604
|
-
yield* drainSettled();
|
|
605
|
-
if (launchError !== null)
|
|
606
|
-
throw launchError;
|
|
607
|
-
// Invariant 3 makes a started commit-required execution in the
|
|
608
|
-
// draft structurally impossible (the turn never committed) —
|
|
609
|
-
// CHECKED, not assumed: if one ever exists, fall back to the
|
|
610
|
-
// pre-F4 abandon (no marker over a started fact, no retry).
|
|
611
|
-
const unsafeStarted = log.all.some((e) => e.type === "tool_execution_started" && e.seq > turnStart && registry.get(e.name)?.effects?.precommitSafe !== true);
|
|
612
|
-
if (log.lastSeq > turnStart && !unsafeStarted) {
|
|
613
|
-
// The loop's THIRD producer of `model_output_abandoned` (an
|
|
614
|
-
// existing variant — no new protocol surface): broader than
|
|
615
|
-
// the live void's dangling-call condition, because the SAME
|
|
616
|
-
// process re-requests immediately and an un-voided text
|
|
617
|
-
// draft would glue onto the retried stream's projection.
|
|
618
|
-
const marker = log.append({
|
|
619
|
-
type: "model_output_abandoned",
|
|
620
|
-
voidFromSeq: turnStart,
|
|
621
|
-
reason: "the provider stream failed before this turn committed",
|
|
622
|
-
});
|
|
623
|
-
if (hooks.onEvent)
|
|
624
|
-
await hooks.onEvent(marker, {}).catch(() => { });
|
|
625
|
-
yield marker;
|
|
626
|
-
}
|
|
622
|
+
// F4 — ABANDON HYGIENE, every streamed exit: settle, drain,
|
|
623
|
+
// durably void (the loop's third `model_output_abandoned`
|
|
624
|
+
// producer; text-only drafts included), and only THEN a retry
|
|
625
|
+
// or the terminal. Pre-F4 this path returned with the draft
|
|
626
|
+
// un-voided under the terminal — the next request projected it
|
|
627
|
+
// as committed history (ADR-0047 Gap B, live), and a dangling
|
|
628
|
+
// tool_call_end fed the provider-400 class (EC1-F1).
|
|
629
|
+
yield* abandonDraft("the provider stream failed before this turn committed");
|
|
627
630
|
// F4 — the mid-stream retry: same classification, same per-turn
|
|
628
631
|
// budget (ADR-0005 Amendment 1: frame state, per-process).
|
|
629
|
-
if (structured.retryable && attempts < maxRetries && !
|
|
632
|
+
if (structured.retryable && attempts < maxRetries && !unsafeStartedInDraft()) {
|
|
630
633
|
attempts += 1;
|
|
631
634
|
await sleep(attempts * 250, signal); // the same abortable backoff
|
|
632
635
|
// Fresh per-attempt state — the marker is the boundary now.
|
|
@@ -725,6 +728,22 @@ export async function* loop(config) {
|
|
|
725
728
|
break;
|
|
726
729
|
}
|
|
727
730
|
}
|
|
731
|
+
// ── MG-1 (A5): the trust boundary's half of the envelope ─────────
|
|
732
|
+
// Stamp, strip, cap — BEFORE the commit decision, so a hard-cap
|
|
733
|
+
// violation voids the turn (no durable stop persists that is known
|
|
734
|
+
// unable to continue correctly; the F4b abandon voids its draft).
|
|
735
|
+
if (voided === null && heldStop !== null && heldStop.continuation !== undefined) {
|
|
736
|
+
const prepared = prepareContinuation(heldStop, config.continuationScope);
|
|
737
|
+
if (prepared === "hard-cap") {
|
|
738
|
+
voided = {
|
|
739
|
+
kind: "error",
|
|
740
|
+
error: { code: "invalid_request", retryable: false, message: "the turn's required continuation metadata exceeds the hard cap" },
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
heldStop = prepared;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
728
747
|
// ── EC-1 ① — TURN COMMIT ──────────────────────────────────────────
|
|
729
748
|
// The held stop is persisted HERE and only here: iterator done (the
|
|
730
749
|
// stream loop broke cleanly) AND structurally compatible (above). The
|
|
@@ -745,7 +764,11 @@ export async function* loop(config) {
|
|
|
745
764
|
// NO calls has nothing to abandon and still ends on its own stop
|
|
746
765
|
// reason, the pre-EC-1 order.
|
|
747
766
|
if (voided === null && pending.length > 0 && aborted()) {
|
|
748
|
-
|
|
767
|
+
// F4b: the abandoned calls are voided LIVE — the resume derives
|
|
768
|
+
// the same void from the same prefix, so live and post-crash
|
|
769
|
+
// projections agree, and no dangling tool_use reaches the next
|
|
770
|
+
// request in either world.
|
|
771
|
+
yield* abandonDraft("the run was aborted before this turn committed");
|
|
749
772
|
yield await terminal({ kind: "aborted", by: "user" });
|
|
750
773
|
return;
|
|
751
774
|
}
|
|
@@ -773,43 +796,28 @@ export async function* loop(config) {
|
|
|
773
796
|
// the in-between gaps (mid-handler launches, the ask pause:
|
|
774
797
|
// never a busy spin, never a deadlock on a pending ack).
|
|
775
798
|
if (voided !== null) {
|
|
776
|
-
|
|
777
|
-
|
|
799
|
+
// EC-1 ⑤ — THE LIVE VOID, F4b: the shared abandon sequence. ①
|
|
800
|
+
// means a voided turn's commit-required call never ran, so nothing
|
|
801
|
+
// answers the `tool_use` its tool_call_end already persisted; the
|
|
802
|
+
// run ends on its terminal and the recovery driver never sees it
|
|
803
|
+
// (its first rule is "the open run reached its terminal") — the
|
|
804
|
+
// provider-400 class, live rather than after a crash. The marker
|
|
805
|
+
// is the instrument the resume already uses, produced here (an
|
|
806
|
+
// existing variant — no new protocol surface). F4b broadened the
|
|
807
|
+
// condition from dangling-calls-only to ANY draft: a text-only
|
|
808
|
+
// voided draft glued onto the next request exactly the way the
|
|
809
|
+
// mid-stream cut's did. A started commit-required call (a FACT)
|
|
810
|
+
// still suppresses the marker — abandonDraft's standing check,
|
|
811
|
+
// the same rule as recovery-plan.ts's `unexecuted`. Idempotent by
|
|
812
|
+
// construction: the marker becomes the last boundary, so no later
|
|
813
|
+
// resume derives a second draft over the same range.
|
|
814
|
+
yield* abandonDraft("the turn was voided before it committed");
|
|
815
|
+
yield await terminal(voided);
|
|
816
|
+
return;
|
|
778
817
|
}
|
|
779
818
|
yield* drainSettled();
|
|
780
819
|
if (launchError !== null)
|
|
781
820
|
throw launchError;
|
|
782
|
-
if (voided !== null) {
|
|
783
|
-
// EC-1 ⑤ — THE LIVE VOID. ① means a voided turn's commit-required
|
|
784
|
-
// call never ran, so nothing answers the `tool_use` its
|
|
785
|
-
// tool_call_end already persisted. The run ends here on its error
|
|
786
|
-
// terminal, and the recovery driver will never see it: its first
|
|
787
|
-
// rule is "the open run reached its terminal". So the NEXT turn of
|
|
788
|
-
// the same session would send the model an assistant tool_use with
|
|
789
|
-
// no result — the provider-400 class, live rather than after a
|
|
790
|
-
// crash. Pre-EC-1 the streaming launch had already answered the
|
|
791
|
-
// pair; closing the destructive hole opened this one.
|
|
792
|
-
//
|
|
793
|
-
// The fix is the instrument the resume already uses, produced here
|
|
794
|
-
// instead: the loop is a SECOND PRODUCER of `model_output_abandoned`
|
|
795
|
-
// (an existing variant — no new protocol surface, the frozen event
|
|
796
|
-
// contract holds). It voids the whole draft range, exactly as
|
|
797
|
-
// ABANDON_DRAFT does, and only when a call is still pure intent —
|
|
798
|
-
// a call with a durable started is a FACT, and the same rule as
|
|
799
|
-
// recovery-plan.ts's `unexecuted`. Idempotent by construction: the
|
|
800
|
-
// marker becomes the last boundary, so no later resume derives a
|
|
801
|
-
// second draft over the same range.
|
|
802
|
-
if (log.all.some((e) => e.type === "tool_call_end" &&
|
|
803
|
-
e.seq > turnStart &&
|
|
804
|
-
!log.all.some((x) => x.type === "tool_execution_started" && x.callId === e.callId))) {
|
|
805
|
-
const marker = log.append({ type: "model_output_abandoned", voidFromSeq: turnStart, reason: "the turn was voided before it committed" });
|
|
806
|
-
if (hooks.onEvent)
|
|
807
|
-
await hooks.onEvent(marker, {}).catch(() => { });
|
|
808
|
-
yield marker;
|
|
809
|
-
}
|
|
810
|
-
yield await terminal(voided);
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
821
|
// ── Advance history: the log grew; re-derive for the next turn ─────
|
|
814
822
|
messages = derive();
|
|
815
823
|
}
|
|
@@ -1282,3 +1290,39 @@ const NEVER_ABORT = {
|
|
|
1282
1290
|
addEventListener: () => { },
|
|
1283
1291
|
removeEventListener: () => { },
|
|
1284
1292
|
};
|
|
1293
|
+
// ── MG-1 (ADR-0051 Amendment 5): the envelope preparation ────────────────
|
|
1294
|
+
//
|
|
1295
|
+
// Pure: stamp the run's scope over whatever the adapter claimed (adapters
|
|
1296
|
+
// are not trusted), strip on an unscoped run, and enforce the two caps —
|
|
1297
|
+
// optional entries drop WHOLE at the soft cap (earliest first, emission
|
|
1298
|
+
// order) with a durable `truncated: true`; a required set over the hard
|
|
1299
|
+
// cap is a provider-contract violation the caller turns into a voided
|
|
1300
|
+
// turn. Sizes are UTF-8 bytes of the serialized entry.
|
|
1301
|
+
const CONTINUATION_SOFT_CAP = 256 * 1024;
|
|
1302
|
+
const CONTINUATION_HARD_CAP = 2 * 1024 * 1024;
|
|
1303
|
+
function prepareContinuation(stop, scope) {
|
|
1304
|
+
const c = stop.continuation;
|
|
1305
|
+
if (c === undefined)
|
|
1306
|
+
return stop;
|
|
1307
|
+
if (scope === undefined) {
|
|
1308
|
+
const { continuation: _stripped, ...rest } = stop;
|
|
1309
|
+
return rest;
|
|
1310
|
+
}
|
|
1311
|
+
const size = (e) => new TextEncoder().encode(e.data).length + new TextEncoder().encode(e.kind).length + 32;
|
|
1312
|
+
const requiredBytes = c.entries.filter((e) => e.required).reduce((n, e) => n + size(e), 0);
|
|
1313
|
+
if (requiredBytes > CONTINUATION_HARD_CAP)
|
|
1314
|
+
return "hard-cap";
|
|
1315
|
+
const kept = [...c.entries];
|
|
1316
|
+
let total = kept.reduce((n, e) => n + size(e), 0);
|
|
1317
|
+
let truncated = false;
|
|
1318
|
+
for (let i = 0; total > CONTINUATION_SOFT_CAP && i < kept.length;) {
|
|
1319
|
+
if (kept[i].required) {
|
|
1320
|
+
i += 1;
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
total -= size(kept[i]);
|
|
1324
|
+
kept.splice(i, 1);
|
|
1325
|
+
truncated = true;
|
|
1326
|
+
}
|
|
1327
|
+
return { ...stop, continuation: { scope, entries: kept, ...(truncated ? { truncated: true } : {}) } };
|
|
1328
|
+
}
|
package/dist/kernel/project.js
CHANGED
|
@@ -87,20 +87,28 @@ export function projectMessages(events) {
|
|
|
87
87
|
// `thinking` events and attached to the assistant message at flush —
|
|
88
88
|
// deterministic (same events → same messages → same request body, D area).
|
|
89
89
|
let pendingReasoning = null;
|
|
90
|
+
// MG-1 (A5): the committed stop's continuation envelope, attached to the
|
|
91
|
+
// SAME flush its stop triggers — only a durable stop carries one, so a
|
|
92
|
+
// voided draft can never leak stale continuation (invariant ③).
|
|
93
|
+
let pendingContinuation = null;
|
|
90
94
|
const flushAssistant = () => {
|
|
91
95
|
pushText();
|
|
92
96
|
if (blocks.length === 0) {
|
|
93
97
|
assistantSource = undefined;
|
|
98
|
+
pendingContinuation = null;
|
|
94
99
|
return;
|
|
95
100
|
}
|
|
96
101
|
const callIds = blocks.filter((b) => b.type === "tool_use").map((b) => b.callId);
|
|
97
102
|
const reasoning = pendingReasoning;
|
|
98
103
|
pendingReasoning = null;
|
|
104
|
+
const continuation = pendingContinuation;
|
|
105
|
+
pendingContinuation = null;
|
|
99
106
|
out.push({
|
|
100
107
|
role: "assistant",
|
|
101
108
|
blocks: [...blocks],
|
|
102
109
|
...(assistantSource !== undefined ? { source: assistantSource } : {}),
|
|
103
110
|
...(reasoning !== null ? { reasoning } : {}),
|
|
111
|
+
...(continuation !== null ? { continuation } : {}),
|
|
104
112
|
});
|
|
105
113
|
blocks = [];
|
|
106
114
|
assistantSource = undefined;
|
|
@@ -159,12 +167,17 @@ export function projectMessages(events) {
|
|
|
159
167
|
// disjoint and in seq order; the skip below treats them exactly like the
|
|
160
168
|
// summary ranges (the marker itself renders nothing and skips itself).
|
|
161
169
|
// R-E 0.1.44 (the void scope sentence): the void range voids MODEL
|
|
162
|
-
// OUTPUT only —
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
170
|
+
// OUTPUT only — never the framework's facts (permission*,
|
|
171
|
+
// tool_execution, tool_result, user_input, terminal). The summary
|
|
172
|
+
// ranges keep their blanket reach (the summary replaces everything it
|
|
173
|
+
// covers). F4b: the block boundaries text_start/text_end ARE model
|
|
174
|
+
// output and join the family — a voided text_start carries the
|
|
175
|
+
// draft's provenance (`source`), and leaving it alive relabeled the
|
|
176
|
+
// RETRIED answer with the abandoned attempt's source.
|
|
166
177
|
const MODEL_OUTPUT_TYPES = new Set([
|
|
178
|
+
"text_start",
|
|
167
179
|
"text_delta",
|
|
180
|
+
"text_end",
|
|
168
181
|
"thinking",
|
|
169
182
|
"tool_call_start",
|
|
170
183
|
"tool_call_input_delta",
|
|
@@ -501,6 +514,9 @@ export function projectMessages(events) {
|
|
|
501
514
|
// projects [assistant, results…] in reading order, and the
|
|
502
515
|
// stream-open guards below only ever flush a CLOSED turn's
|
|
503
516
|
// late (post-stop) results.
|
|
517
|
+
// MG-1 (A5): the committed stop hands its envelope to the
|
|
518
|
+
// flush it triggers; every other flush site carries none.
|
|
519
|
+
pendingContinuation = ev.continuation ?? null;
|
|
504
520
|
flushAssistant();
|
|
505
521
|
flushResults();
|
|
506
522
|
break;
|
|
@@ -46,6 +46,13 @@ export interface StreamOptions {
|
|
|
46
46
|
readonly tools?: readonly ToolSpec[];
|
|
47
47
|
readonly maxTokens?: number;
|
|
48
48
|
readonly temperature?: number;
|
|
49
|
+
/** XP-1: the RESOLVED reasoning setting — native wire values only (the
|
|
50
|
+
* runtime's matrix resolves and refuses; adapters serialize per
|
|
51
|
+
* dialect). Absent = provider defaults, byte-identical to pre-XP-1. */
|
|
52
|
+
readonly reasoning?: {
|
|
53
|
+
readonly thinking?: "adaptive" | "enabled" | "disabled";
|
|
54
|
+
readonly effort?: string;
|
|
55
|
+
};
|
|
49
56
|
readonly signal?: AbortSignalLike;
|
|
50
57
|
}
|
|
51
58
|
/**
|
|
@@ -417,10 +417,43 @@ export interface Usage {
|
|
|
417
417
|
readonly cacheWrite: number | null;
|
|
418
418
|
readonly known: boolean;
|
|
419
419
|
}
|
|
420
|
+
/** MG-1 (ADR-0051 Amendment 5): the continuation envelope's scope — WHO
|
|
421
|
+
* may replay it. Kernel-stamped at the Turn Commit append from the run's
|
|
422
|
+
* configured binding; an adapter-supplied scope is always overwritten
|
|
423
|
+
* (adapters are not trusted), and a run with no configured scope has
|
|
424
|
+
* adapter-emitted continuation stripped at the same boundary. */
|
|
425
|
+
export interface ContinuationScope {
|
|
426
|
+
readonly providerId: string;
|
|
427
|
+
readonly apiId: string;
|
|
428
|
+
readonly modelId: string;
|
|
429
|
+
/** Origin only; REQUIRED when providerId === "custom". */
|
|
430
|
+
readonly endpoint?: string;
|
|
431
|
+
}
|
|
432
|
+
/** One opaque provider block. `data` is bytes to the kernel — serialized
|
|
433
|
+
* verbatim by the emitting adapter, replayed verbatim by the scope-matched
|
|
434
|
+
* one, never reconstructed from projected text. */
|
|
435
|
+
export interface ContinuationEntry {
|
|
436
|
+
readonly kind: string;
|
|
437
|
+
/** true = the next request is INVALID without it (never dropped; a
|
|
438
|
+
* required set over the hard cap voids the turn before commit).
|
|
439
|
+
* false = quality-degradable (droppable under the soft cap). */
|
|
440
|
+
readonly required: boolean;
|
|
441
|
+
readonly data: string;
|
|
442
|
+
}
|
|
443
|
+
export interface Continuation {
|
|
444
|
+
readonly scope: ContinuationScope;
|
|
445
|
+
/** EMISSION ORDER, preserved end to end. */
|
|
446
|
+
readonly entries: readonly ContinuationEntry[];
|
|
447
|
+
/** Present only when OPTIONAL entries were dropped at the soft cap. */
|
|
448
|
+
readonly truncated?: true;
|
|
449
|
+
}
|
|
420
450
|
export interface Stop {
|
|
421
451
|
readonly seq: number;
|
|
422
452
|
readonly type: "stop";
|
|
423
453
|
readonly reason: StopReason;
|
|
454
|
+
/** MG-1 (Amendment 5): absent on every pre-A5 log — rule 1's truly
|
|
455
|
+
* optional field; old logs project byte-identically. */
|
|
456
|
+
readonly continuation?: Continuation;
|
|
424
457
|
}
|
|
425
458
|
/**
|
|
426
459
|
* Structured failure classification for MODEL / TRANSPORT errors.
|
package/dist/protocol/events.js
CHANGED
|
@@ -143,6 +143,33 @@ function isTerminal(v) {
|
|
|
143
143
|
* safe integer; the others are null when the provider did not report
|
|
144
144
|
* them.
|
|
145
145
|
*/
|
|
146
|
+
/** A5: shape-checked ONLY when present — rule 1's truly-optional clause.
|
|
147
|
+
* The key set stays open, so older bins load newer logs unchanged. */
|
|
148
|
+
function isContinuation(v) {
|
|
149
|
+
if (v === undefined)
|
|
150
|
+
return true;
|
|
151
|
+
if (typeof v !== "object" || v === null)
|
|
152
|
+
return false;
|
|
153
|
+
const c = v;
|
|
154
|
+
const s = c.scope;
|
|
155
|
+
if (typeof s !== "object" || s === null)
|
|
156
|
+
return false;
|
|
157
|
+
const sc = s;
|
|
158
|
+
if (typeof sc.providerId !== "string" || typeof sc.apiId !== "string" || typeof sc.modelId !== "string")
|
|
159
|
+
return false;
|
|
160
|
+
if (sc.endpoint !== undefined && typeof sc.endpoint !== "string")
|
|
161
|
+
return false;
|
|
162
|
+
if (!Array.isArray(c.entries))
|
|
163
|
+
return false;
|
|
164
|
+
for (const e of c.entries) {
|
|
165
|
+
if (typeof e !== "object" || e === null)
|
|
166
|
+
return false;
|
|
167
|
+
const r = e;
|
|
168
|
+
if (typeof r.kind !== "string" || typeof r.required !== "boolean" || typeof r.data !== "string")
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return c.truncated === undefined || c.truncated === true;
|
|
172
|
+
}
|
|
146
173
|
function isUsage(v) {
|
|
147
174
|
if (typeof v.known !== "boolean")
|
|
148
175
|
return false;
|
|
@@ -176,7 +203,7 @@ const EVENT_VALIDATORS = {
|
|
|
176
203
|
isTags(v) && isInvocationSeq(v),
|
|
177
204
|
thinking: (v) => typeof v.text === "string",
|
|
178
205
|
usage: isUsage,
|
|
179
|
-
stop: (v) => STOP_REASONS.has(v.reason),
|
|
206
|
+
stop: (v) => STOP_REASONS.has(v.reason) && isContinuation(v.continuation),
|
|
180
207
|
user_input: (v) => isContent(v.content) && isSource(v),
|
|
181
208
|
compacted: (v) => Array.isArray(v.cleared) &&
|
|
182
209
|
v.cleared.every((c) => isPlainObject(c) &&
|
|
@@ -82,6 +82,13 @@ export interface AssistantMessage {
|
|
|
82
82
|
* follow-up requests). Present only when the turn actually reasoned.
|
|
83
83
|
*/
|
|
84
84
|
readonly reasoning?: string;
|
|
85
|
+
/**
|
|
86
|
+
* MG-1 (ADR-0051 Amendment 5): the turn's committed continuation
|
|
87
|
+
* envelope, derived from its stop. Opaque to everything but the
|
|
88
|
+
* scope-matched adapter; messages are the versioned model-request
|
|
89
|
+
* side (Amendment 3(a)), never the frozen plane.
|
|
90
|
+
*/
|
|
91
|
+
readonly continuation?: import("./events.js").Continuation;
|
|
85
92
|
}
|
|
86
93
|
/**
|
|
87
94
|
* Sent back to the model after a tool ran.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "kiso (foundation) core \u2014 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.16.0",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|