@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.
- package/dist/checkpoint.d.ts +35 -0
- package/dist/checkpoint.js +98 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +4 -0
- package/dist/request-budget.d.ts +46 -0
- package/dist/request-budget.js +45 -0
- package/dist/session.d.ts +11 -0
- package/dist/session.js +13 -0
- package/dist/summarize.d.ts +4 -0
- package/dist/summarize.js +1 -1
- package/package.json +5 -5
|
@@ -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
|
+
}
|
package/dist/internal.d.ts
CHANGED
|
@@ -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;
|
package/dist/summarize.d.ts
CHANGED
|
@@ -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.
|
|
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
|
+
"@vincemakes/kiso-core": "0.29.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "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.
|
|
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"
|