@vincemakes/kiso-runtime 0.27.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";
@@ -29,6 +29,74 @@ const DEEPSEEK_PRICING = {
29
29
  asOf: "2026-08-13",
30
30
  source: "https://api-docs.deepseek.com/quick_start/pricing",
31
31
  };
32
+ /** PA-1a: the Anthropic rates as read on 2026-09-07 (base input, output,
33
+ * cache read = "cache hits and refreshes", cache write = the 5-minute
34
+ * write). The pricing page notes Sonnet 5's $2/$10 is the standard
35
+ * price (the announced September increase did not occur). */
36
+ const ANTHROPIC_PRICING_SOURCE = "https://platform.claude.com/docs/en/about-claude/pricing";
37
+ const ANTHROPIC_MODELS_SOURCE = "https://platform.claude.com/docs/en/models/overview";
38
+ const ANTHROPIC_EFFORT_SOURCE = "https://platform.claude.com/docs/en/build-with-claude/effort";
39
+ const ANTHROPIC_ASOF = "2026-09-07";
40
+ const anthropicPricing = (inputPerM, outputPerM, cacheReadPerM, cacheWritePerM) => ({
41
+ inputPerM,
42
+ outputPerM,
43
+ cacheReadPerM,
44
+ cacheWritePerM,
45
+ asOf: ANTHROPIC_ASOF,
46
+ source: ANTHROPIC_PRICING_SOURCE,
47
+ });
48
+ /** The 5-line's effort control: five native levels, default high, wire
49
+ * output_config.effort (the effort page, read 2026-09-07). The thinking
50
+ * table (https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting)
51
+ * gives the modes per model. */
52
+ const FIVE_LEVELS = { levels: ["low", "medium", "high", "xhigh", "max"], default: "high", wire: "output_config.effort" };
53
+ function anthropicLine() {
54
+ const row = (model, capabilities, pricing) => ({
55
+ model,
56
+ providerId: "anthropic",
57
+ capabilities,
58
+ capabilitiesAsOf: ANTHROPIC_ASOF,
59
+ capabilitiesSource: ANTHROPIC_MODELS_SOURCE,
60
+ pricing,
61
+ });
62
+ const fiveLine = (thinkingModes, forbidden) => ({
63
+ contextWindow: 1_000_000,
64
+ maxOutputTokens: 128_000,
65
+ promptCaching: "explicit",
66
+ reasoning: {
67
+ emitsThinkingStream: true,
68
+ thinking: { modes: thinkingModes, default: "adaptive" },
69
+ effort: FIVE_LEVELS,
70
+ ...(forbidden !== undefined ? { forbidden } : {}),
71
+ asOf: ANTHROPIC_ASOF,
72
+ source: ANTHROPIC_EFFORT_SOURCE,
73
+ },
74
+ inputModalities: ["text", "image"],
75
+ });
76
+ const haiku = {
77
+ contextWindow: 200_000,
78
+ maxOutputTokens: 64_000,
79
+ promptCaching: "explicit",
80
+ // Extended thinking only (budget_tokens; adaptive rejected) — the
81
+ // XP-1 setting has no budget axis, so the toggle is not driven;
82
+ // effort: "Not supported" on the models overview. Unknown stays null.
83
+ reasoning: { emitsThinkingStream: true, thinking: null, effort: null, asOf: ANTHROPIC_ASOF, source: ANTHROPIC_EFFORT_SOURCE },
84
+ inputModalities: ["text", "image"],
85
+ };
86
+ const haikuPricing = anthropicPricing(1, 5, 0.1, 1.25);
87
+ return [
88
+ // Fable 5.1: adaptive only, ALWAYS on — "enabled" and "disabled" both 400.
89
+ row("claude-fable-5-1", fiveLine(["adaptive"]), anthropicPricing(10, 50, 0.25, 12.5)),
90
+ // Opus 5: adaptive, on by default; "disabled" accepted at effort high
91
+ // or below — with xhigh or max it is a 400 (the forbidden pair).
92
+ row("claude-opus-5", fiveLine(["adaptive", "disabled"], [{ thinking: "disabled", effort: "xhigh" }, { thinking: "disabled", effort: "max" }]), anthropicPricing(5, 25, 0.5, 6.25)),
93
+ // Sonnet 5: adaptive, on by default; "disabled" accepted.
94
+ row("claude-sonnet-5", fiveLine(["adaptive", "disabled"]), anthropicPricing(2, 10, 0.2, 2.5)),
95
+ // Haiku 4.5: the pinned id and its alias (the registry matches exact ids).
96
+ row("claude-haiku-4-5-20251001", haiku, haikuPricing),
97
+ row("claude-haiku-4-5", haiku, haikuPricing),
98
+ ];
99
+ }
32
100
  /** The v1 table. Nulls outnumber numbers ON PURPOSE: only values with a
33
101
  * named source enter; everything else waits for one. */
34
102
  const ENTRIES = [
@@ -54,22 +122,16 @@ const ENTRIES = [
54
122
  capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: { emitsThinkingStream: true, thinking: null, effort: null, asOf: null, source: null }, inputModalities: null },
55
123
  pricing: DEEPSEEK_PRICING,
56
124
  },
57
- {
58
- model: "claude-sonnet-5",
59
- providerId: "anthropic",
60
- capabilities: { contextWindow: 200_000, maxOutputTokens: null, promptCaching: "explicit",
61
- reasoning: {
62
- emitsThinkingStream: true,
63
- thinking: null,
64
- effort: { levels: ["low", "medium", "high", "xhigh", "max"], default: "high", wire: "output_config.effort" },
65
- asOf: "2026-08-26",
66
- source: "https://platform.claude.com/docs/en/build-with-claude/effort",
67
- },
68
- inputModalities: null },
69
- // Priced only when the rates are read from the live billing page
70
- // and dated — never copied from memory (the review's boundary ②).
71
- pricing: null,
72
- },
125
+ // PA-1a (2026-09-07): the Anthropic first-party line, read from the live
126
+ // docs that day — the models overview (ids, context, max output,
127
+ // modalities, thinking type, default effort), the thinking table
128
+ // (accepted / rejected thinking.type per model, the Opus 5 forbidden
129
+ // pair), the effort page (levels), the pricing page (rates). Each row
130
+ // carries the dates; nothing here is from memory. Extended thinking
131
+ // (`enabled` + budget_tokens) is registered for NO model: the 5-line
132
+ // rejects it, and Haiku 4.5's manual-budget mode has no axis in the
133
+ // XP-1 setting — recorded as `thinking: null` on that row, never guessed.
134
+ ...anthropicLine(),
73
135
  {
74
136
  // XP-1: the current DeepSeek line — the model every RD-1 benchmark
75
137
  // artifact was produced with, previously ABSENT (null pricing and
@@ -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.27.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.27.0"
28
+ "@vincemakes/kiso-core": "0.29.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.27.0",
32
- "@vincemakes/kiso-provider-openai": "0.27.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.27.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"