@vincemakes/kiso-runtime 0.28.0 → 0.29.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.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A1a (C) — the checkpoint boundary at a SETTLED model turn.
3
+ *
4
+ * `summaryBoundarySeq` can only cut just before a `user_input` round; one
5
+ * instruction followed by hundreds of model/tool turns therefore never
6
+ * yields a boundary (roadmap R3.1, the audit's A1). This is the DEFINITION
7
+ * of a cut that lives inside such a run — a pure projection over the log,
8
+ * with the invariants stated, and nothing in production calls it until
9
+ * A1b chooses when (the policy is not decided here).
10
+ *
11
+ * A candidate is a model turn whose `stop` is committed and whose every
12
+ * `tool_call_end` has its answer — a `tool_result` paired by
13
+ * `invocationSeq` (the CX-1 F1 lesson: a callId is correlation, never
14
+ * identity; callId pairs only legacy results that carry no invocationSeq,
15
+ * and only after the call) — or lies in a void range (a voided draft's
16
+ * calls never ran and are not unanswered). The cut is the settled turn's
17
+ * LAST event: its last result, or its stop when it made no calls. EC-1
18
+ * persists the stop BEFORE the results land, so a cut at `stop.seq` would
19
+ * cover the call and keep the result — the straddle the whole-round
20
+ * pullback exists to prevent; here it cannot happen by construction.
21
+ *
22
+ * Invariants: the cut lies after the last summary point; at least one
23
+ * settled turn stays after it (the model keeps its most recent turn);
24
+ * the turn holding the LATEST do-not-compact result is never covered
25
+ * (the cut stays before that turn — per model turn, not per user round);
26
+ * the kept suffix meets `keepTokens` (the same event-token estimate the
27
+ * whole-round walk uses). No candidate → undefined, the honest "nothing
28
+ * to checkpoint".
29
+ */
30
+ import type { Event } from "@vincemakes/kiso-core";
31
+ export interface CheckpointOptions {
32
+ /** the minimum estimated tokens that must remain AFTER the cut (default 0). */
33
+ readonly keepTokens?: number;
34
+ }
35
+ export declare function checkpointBoundarySeq(events: readonly Event[], opts?: CheckpointOptions): number | undefined;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * A1a (C) — the checkpoint boundary at a SETTLED model turn.
3
+ *
4
+ * `summaryBoundarySeq` can only cut just before a `user_input` round; one
5
+ * instruction followed by hundreds of model/tool turns therefore never
6
+ * yields a boundary (roadmap R3.1, the audit's A1). This is the DEFINITION
7
+ * of a cut that lives inside such a run — a pure projection over the log,
8
+ * with the invariants stated, and nothing in production calls it until
9
+ * A1b chooses when (the policy is not decided here).
10
+ *
11
+ * A candidate is a model turn whose `stop` is committed and whose every
12
+ * `tool_call_end` has its answer — a `tool_result` paired by
13
+ * `invocationSeq` (the CX-1 F1 lesson: a callId is correlation, never
14
+ * identity; callId pairs only legacy results that carry no invocationSeq,
15
+ * and only after the call) — or lies in a void range (a voided draft's
16
+ * calls never ran and are not unanswered). The cut is the settled turn's
17
+ * LAST event: its last result, or its stop when it made no calls. EC-1
18
+ * persists the stop BEFORE the results land, so a cut at `stop.seq` would
19
+ * cover the call and keep the result — the straddle the whole-round
20
+ * pullback exists to prevent; here it cannot happen by construction.
21
+ *
22
+ * Invariants: the cut lies after the last summary point; at least one
23
+ * settled turn stays after it (the model keeps its most recent turn);
24
+ * the turn holding the LATEST do-not-compact result is never covered
25
+ * (the cut stays before that turn — per model turn, not per user round);
26
+ * the kept suffix meets `keepTokens` (the same event-token estimate the
27
+ * whole-round walk uses). No candidate → undefined, the honest "nothing
28
+ * to checkpoint".
29
+ */
30
+ import { DO_NOT_COMPACT } from "@vincemakes/kiso-core";
31
+ import { estimateEventTokens, lastSummaryPoint } from "./summarize.js";
32
+ const MODEL_EVENT = new Set(["text_start", "text_delta", "text_end", "thinking", "tool_call_start", "tool_call_input_delta", "tool_call_end"]);
33
+ export function checkpointBoundarySeq(events, opts = {}) {
34
+ const keepTokens = opts.keepTokens ?? 0;
35
+ const prevPoint = lastSummaryPoint(events);
36
+ const voids = [];
37
+ for (const e of events)
38
+ if (e.type === "model_output_abandoned")
39
+ voids.push({ from: e.voidFromSeq, to: e.seq });
40
+ const voided = (seq) => voids.some((r) => seq > r.from && seq <= r.to);
41
+ const live = events.filter((e) => e.seq > prevPoint && !voided(e.seq));
42
+ // the answers: by invocationSeq (round 5+), by callId only for legacy results
43
+ const byInvocation = new Map();
44
+ const legacyByCallId = new Map();
45
+ for (const e of live) {
46
+ if (e.type !== "tool_result")
47
+ continue;
48
+ if (e.invocationSeq !== undefined)
49
+ byInvocation.set(e.invocationSeq, e);
50
+ else
51
+ (legacyByCallId.get(e.callId) ?? legacyByCallId.set(e.callId, []).get(e.callId)).push(e);
52
+ }
53
+ const answerOf = (call) => byInvocation.get(call.seq) ?? legacyByCallId.get(call.callId)?.find((r) => r.seq > call.seq);
54
+ // the model turns: from the first model event after a user_input / stop to the committed stop
55
+ const turns = [];
56
+ let start = null;
57
+ let calls = [];
58
+ for (const e of live) {
59
+ if (e.type === "user_input") {
60
+ start = null;
61
+ calls = [];
62
+ continue;
63
+ }
64
+ if (start === null && MODEL_EVENT.has(e.type))
65
+ start = e.seq;
66
+ if (e.type === "tool_call_end")
67
+ calls.push(e);
68
+ if (e.type !== "stop")
69
+ continue;
70
+ const answers = calls.map(answerOf);
71
+ const settled = answers.every((a) => a !== undefined);
72
+ const cut = Math.max(e.seq, ...answers.map((a) => a?.seq ?? -1));
73
+ const protectedTurn = answers.some((a) => a !== undefined && (a.tags ?? []).includes(DO_NOT_COMPACT));
74
+ turns.push({ start: start ?? e.seq, cut, settled, protectedTurn });
75
+ start = null;
76
+ calls = [];
77
+ }
78
+ // the latest do-not-compact turn is never covered: every cut stays before it
79
+ const lastProtected = [...turns].reverse().find((t) => t.protectedTurn);
80
+ const ceiling = lastProtected === undefined ? Number.POSITIVE_INFINITY : lastProtected.start;
81
+ let total = 0;
82
+ const tokensAfter = new Map(); // cut -> tokens of live events after it
83
+ for (let i = live.length - 1; i >= 0; i -= 1) {
84
+ tokensAfter.set(live[i].seq, total);
85
+ total += estimateEventTokens(live[i]);
86
+ }
87
+ const settledIdx = turns.map((t, i) => (t.settled ? i : -1)).filter((i) => i >= 0);
88
+ // a candidate must leave at least one settled turn after it
89
+ for (let k = settledIdx.length - 2; k >= 0; k -= 1) {
90
+ const t = turns[settledIdx[k]];
91
+ if (t.cut >= ceiling)
92
+ continue;
93
+ if ((tokensAfter.get(t.cut) ?? 0) < keepTokens)
94
+ continue;
95
+ return t.cut;
96
+ }
97
+ return undefined;
98
+ }
@@ -26,3 +26,5 @@ export * from "./trust.js";
26
26
  export * from "./provider/metadata.js";
27
27
  export { canonicalizeUsageForModel } from "./usage/canonical.js";
28
28
  export { BUILTIN_MANIFESTS, resolveContinuationScope, type ModelRef, type ProviderManifest } from "./provider/manifest.js";
29
+ export * from "./checkpoint.js";
30
+ export * from "./request-budget.js";
package/dist/internal.js CHANGED
@@ -32,3 +32,7 @@ export * from "./provider/metadata.js";
32
32
  export { canonicalizeUsageForModel } from "./usage/canonical.js";
33
33
  // MG-1 (A5): the identity layer — manifests and the scope resolver.
34
34
  export { BUILTIN_MANIFESTS, resolveContinuationScope } from "./provider/manifest.js";
35
+ // A1a: the checkpoint boundary (a definition — nothing fires it until A1b)
36
+ // and the request-budget accounting the CLI's status line reads.
37
+ export * from "./checkpoint.js";
38
+ export * from "./request-budget.js";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A1a (A) — request-budget accounting: the parts of the request the
3
+ * kernel assembles (system prompt, tool table, projected messages with
4
+ * their continuation envelopes, the output reserve), each estimated the
5
+ * way the kernel estimates (chars / 4 — a proxy, marked approximate),
6
+ * summed ONCE. The CLI's `ctx left` had measured the projected messages
7
+ * alone, so a fresh session with a large tool table read "~100%" — the
8
+ * headroom was never that.
9
+ *
10
+ * Two definitional points (the 2026-09-07 review): the continuation
11
+ * envelopes ride INSIDE the projected assistant messages and are
12
+ * measured as a sub-part of `messages`, never added on top; the output
13
+ * reserve is the request's `max_tokens` when a profile or provider sets
14
+ * one and NULL when unknown (the openai-compat path sends none unless
15
+ * configured) — the total then excludes it and says so.
16
+ *
17
+ * This is a display and design entry. The auto-compact policy keeps its
18
+ * own number (`estimateCtxRatio` in the CLI) — moving it is A1b's.
19
+ */
20
+ import type { Message, ToolSpec } from "@vincemakes/kiso-core";
21
+ export interface RequestParts {
22
+ readonly systemPrompt?: string;
23
+ readonly toolSpecs: readonly ToolSpec[];
24
+ readonly messages: readonly Message[];
25
+ /** the request's max_tokens; undefined/null = the provider sends none */
26
+ readonly maxTokens?: number | null;
27
+ }
28
+ export interface RequestBudget {
29
+ readonly approximate: true;
30
+ readonly system: number;
31
+ readonly tools: number;
32
+ readonly user: number;
33
+ readonly assistant: number;
34
+ readonly toolResults: number;
35
+ /** the continuation envelopes inside the assistant messages (a sub-part of `messages`) */
36
+ readonly continuations: number;
37
+ /** user + assistant + toolResults + continuations */
38
+ readonly messages: number;
39
+ readonly outputReserve: number | null;
40
+ /** system + tools + messages + (outputReserve ?? 0) */
41
+ readonly total: number;
42
+ readonly window: number;
43
+ readonly headroom: number;
44
+ readonly ratio: number;
45
+ }
46
+ export declare function requestBudget(parts: RequestParts, window: number): RequestBudget;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A1a (A) — request-budget accounting: the parts of the request the
3
+ * kernel assembles (system prompt, tool table, projected messages with
4
+ * their continuation envelopes, the output reserve), each estimated the
5
+ * way the kernel estimates (chars / 4 — a proxy, marked approximate),
6
+ * summed ONCE. The CLI's `ctx left` had measured the projected messages
7
+ * alone, so a fresh session with a large tool table read "~100%" — the
8
+ * headroom was never that.
9
+ *
10
+ * Two definitional points (the 2026-09-07 review): the continuation
11
+ * envelopes ride INSIDE the projected assistant messages and are
12
+ * measured as a sub-part of `messages`, never added on top; the output
13
+ * reserve is the request's `max_tokens` when a profile or provider sets
14
+ * one and NULL when unknown (the openai-compat path sends none unless
15
+ * configured) — the total then excludes it and says so.
16
+ *
17
+ * This is a display and design entry. The auto-compact policy keeps its
18
+ * own number (`estimateCtxRatio` in the CLI) — moving it is A1b's.
19
+ */
20
+ import { estimateTokens } from "@vincemakes/kiso-core";
21
+ const chars = (s) => Math.ceil(s.length / 4);
22
+ export function requestBudget(parts, window) {
23
+ const system = parts.systemPrompt === undefined ? 0 : chars(parts.systemPrompt);
24
+ const tools = parts.toolSpecs.length === 0 ? 0 : chars(JSON.stringify(parts.toolSpecs));
25
+ let user = 0;
26
+ let assistant = 0;
27
+ let toolResults = 0;
28
+ let continuations = 0;
29
+ for (const m of parts.messages) {
30
+ const t = estimateTokens([m]);
31
+ if (m.role === "user")
32
+ user += t;
33
+ else if (m.role === "assistant") {
34
+ assistant += t;
35
+ if (m.continuation !== undefined)
36
+ continuations += chars(JSON.stringify(m.continuation));
37
+ }
38
+ else
39
+ toolResults += t;
40
+ }
41
+ const messages = user + assistant + toolResults + continuations;
42
+ const outputReserve = parts.maxTokens ?? null;
43
+ const total = system + tools + messages + (outputReserve ?? 0);
44
+ return { approximate: true, system, tools, user, assistant, toolResults, continuations, messages, outputReserve, total, window, headroom: window - total, ratio: total / window };
45
+ }
package/dist/session.d.ts CHANGED
@@ -147,6 +147,17 @@ export declare class AgentSession {
147
147
  * binding, restored from the durable profile on open. The status row
148
148
  * reads THIS, so the row and the request can never disagree. */
149
149
  get model(): string;
150
+ /** A1a: the parts of the NEXT request as the kernel would assemble them —
151
+ * the system prompt, the tool table (the same snapshot the loop sends:
152
+ * live extension tools included), the projected messages with their
153
+ * continuation envelopes, and the request's max_tokens (absent when
154
+ * the provider sends none). For `requestBudget` — accounting, no policy. */
155
+ requestParts(): {
156
+ readonly systemPrompt?: string;
157
+ readonly toolSpecs: readonly import("@vincemakes/kiso-core").ToolSpec[];
158
+ readonly messages: readonly Message[];
159
+ readonly maxTokens?: number;
160
+ };
150
161
  /** XP-1: the selected reasoning axes (resolution happens per request). */
151
162
  get reasoning(): ReasoningSetting;
152
163
  /**
package/dist/session.js CHANGED
@@ -235,6 +235,19 @@ export class AgentSession {
235
235
  get model() {
236
236
  return this.#model;
237
237
  }
238
+ /** A1a: the parts of the NEXT request as the kernel would assemble them —
239
+ * the system prompt, the tool table (the same snapshot the loop sends:
240
+ * live extension tools included), the projected messages with their
241
+ * continuation envelopes, and the request's max_tokens (absent when
242
+ * the provider sends none). For `requestBudget` — accounting, no policy. */
243
+ requestParts() {
244
+ return {
245
+ ...(this.#config.systemPrompt !== undefined ? { systemPrompt: this.#config.systemPrompt } : {}),
246
+ toolSpecs: this.#config.registry.snapshot().specs,
247
+ messages: this.projected(),
248
+ ...(this.#config.maxTokens !== undefined ? { maxTokens: this.#config.maxTokens } : {}),
249
+ };
250
+ }
238
251
  /** XP-1: the selected reasoning axes (resolution happens per request). */
239
252
  get reasoning() {
240
253
  return this.#reasoning;
@@ -140,6 +140,10 @@ export declare const DROP_PLACEHOLDER = "[e6-crux: the covered turns were droppe
140
140
  * the next summary runs from here.
141
141
  */
142
142
  export declare function lastSummaryPoint(events: readonly Event[]): number;
143
+ /** The chars/4 token proxy for a single EVENT (the same convention as
144
+ * estimateTokens, event-shaped — the (f) keep-floor walk needs the kept
145
+ * suffix's tokens without projecting it). */
146
+ export declare function estimateEventTokens(ev: Event): number;
143
147
  /**
144
148
  * The covered range's end: the seq of the event just before the
145
149
  * keepRounds-th most recent user_input AFTER the last summary point —
package/dist/summarize.js CHANGED
@@ -272,7 +272,7 @@ export function lastSummaryPoint(events) {
272
272
  /** The chars/4 token proxy for a single EVENT (the same convention as
273
273
  * estimateTokens, event-shaped — the (f) keep-floor walk needs the kept
274
274
  * suffix's tokens without projecting it). */
275
- function estimateEventTokens(ev) {
275
+ export function estimateEventTokens(ev) {
276
276
  switch (ev.type) {
277
277
  case "user_input":
278
278
  return Math.ceil(ev.content.length / 4);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "kiso runtime — durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,11 +25,11 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@vincemakes/kiso-core": "0.28.0"
28
+ "@vincemakes/kiso-core": "0.29.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.28.0",
32
- "@vincemakes/kiso-provider-openai": "0.28.0"
31
+ "@vincemakes/kiso-provider-anthropic": "0.29.0",
32
+ "@vincemakes/kiso-provider-openai": "0.29.0"
33
33
  },
34
34
  "peerDependenciesMeta": {
35
35
  "@vincemakes/kiso-provider-anthropic": {
@@ -40,7 +40,7 @@
40
40
  }
41
41
  },
42
42
  "devDependencies": {
43
- "@vincemakes/kiso-evals": "0.28.0",
43
+ "@vincemakes/kiso-evals": "0.29.0",
44
44
  "@types/node": "^26.1.2",
45
45
  "typescript": "^5.7.2",
46
46
  "vitest": "^3.0.0"