@vincemakes/kiso-core 0.15.12 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.
@@ -527,6 +527,7 @@ export async function* loop(config) {
527
527
  const stream = config.adapter.stream({
528
528
  model: config.model,
529
529
  messages,
530
+ ...(config.reasoning !== undefined ? { reasoning: config.reasoning } : {}),
530
531
  ...(config.systemPrompt !== undefined ? { systemPrompt: config.systemPrompt } : {}),
531
532
  tools: registry.toSpecs(),
532
533
  ...(config.maxTokens !== undefined ? { maxTokens: config.maxTokens } : {}),
@@ -727,6 +728,22 @@ export async function* loop(config) {
727
728
  break;
728
729
  }
729
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
+ }
730
747
  // ── EC-1 ① — TURN COMMIT ──────────────────────────────────────────
731
748
  // The held stop is persisted HERE and only here: iterator done (the
732
749
  // stream loop broke cleanly) AND structurally compatible (above). The
@@ -1273,3 +1290,39 @@ const NEVER_ABORT = {
1273
1290
  addEventListener: () => { },
1274
1291
  removeEventListener: () => { },
1275
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
+ }
@@ -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;
@@ -506,6 +514,9 @@ export function projectMessages(events) {
506
514
  // projects [assistant, results…] in reading order, and the
507
515
  // stream-open guards below only ever flush a CLOSED turn's
508
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;
509
520
  flushAssistant();
510
521
  flushResults();
511
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.
@@ -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.15.12",
3
+ "version": "0.16.1",
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.15.12",
36
+ "@vincemakes/kiso-evals": "0.16.1",
37
37
  "@types/node": "^26.1.2",
38
38
  "typescript": "^5.7.2",
39
39
  "vitest": "^3.0.0"