@vincemakes/kiso-runtime 0.39.0 → 0.39.2

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/session.d.ts CHANGED
@@ -30,7 +30,7 @@
30
30
  import { EventLog, type AbortSignalLike, type Adapter, type Event, type KisoExtension, type Message, type PermissionDecision, type Tool } from "@vincemakes/kiso-core";
31
31
  import { type TaskAssessment } from "./task-assessment.js";
32
32
  import { type ContinuationScope } from "@vincemakes/kiso-core";
33
- import type { ReasoningSetting } from "./provider/metadata.js";
33
+ import { type ReasoningSetting } from "./provider/metadata.js";
34
34
  import { type SessionStore } from "./store.js";
35
35
  import { Run } from "./run.js";
36
36
  /** TUI2-R3v2 ③ — one off-trajectory model request (session.sideQuery).
@@ -89,7 +89,6 @@ export interface CompactInfo {
89
89
  /** The covered content's estimated tokens (the chars/4 proxy). */
90
90
  readonly tokens: number;
91
91
  }
92
- /** @deprecated the canonical name is `Session` (root export, 1.1.0); this alias is removed in the next major. */
93
92
  export declare class AgentSession {
94
93
  #private;
95
94
  readonly id: string;
@@ -265,6 +264,7 @@ export declare class AgentSession {
265
264
  onStart?: (info: CompactInfo) => void;
266
265
  drop?: boolean;
267
266
  focus?: string;
267
+ manualBudget?: boolean;
268
268
  }): Promise<SummarizeResult | null>;
269
269
  /**
270
270
  * E6 — the run-start context policy (candidate A + the crux drop arm).
package/dist/session.js CHANGED
@@ -36,7 +36,8 @@ import { assessTasks } from "./task-assessment.js";
36
36
  const DEFAULT_EVIDENCE_TOOLS = new Set(["shell"]);
37
37
  import { denialResult } from "@vincemakes/kiso-core";
38
38
  import { buildProfile, readProfile, writeProfile } from "./profile.js";
39
- import { DROP_PLACEHOLDER, estimateSummarySavings, KEEP_RECENT_ROUNDS, KEEP_TOKENS_DEFAULT, lastSummaryPoint, MAX_SUMMARY_FAILURES, policyTriggerFromWindow, serializeCovered, SUMMARY_MAX_OUTPUT, summarizeConversation, summaryBoundarySeq, } from "./summarize.js";
39
+ import { resolveReasoning } from "./provider/metadata.js";
40
+ import { DROP_PLACEHOLDER, estimateSummarySavings, KEEP_RECENT_ROUNDS, KEEP_TOKENS_DEFAULT, lastSummaryPoint, MAX_SUMMARY_FAILURES, policyTriggerFromWindow, serializeCovered, MANUAL_SUMMARY_BUDGET, SUMMARY_MAX_OUTPUT, summarizeConversation, summaryBoundarySeq, } from "./summarize.js";
40
41
  import { canonicalizeUsageForModel } from "./usage/canonical.js";
41
42
  import { appendFileSync, mkdirSync } from "node:fs";
42
43
  import { join } from "node:path";
@@ -69,6 +70,24 @@ export class ResumeBlockedError extends Error {
69
70
  }
70
71
  }
71
72
  /** @deprecated the canonical name is `Session` (root export, 1.1.0); this alias is removed in the next major. */
73
+ /**
74
+ * 0.39.2 — thinking OFF for a manual summary, where it can be turned off.
75
+ *
76
+ * The summary call sent no reasoning at all, so it ran at the provider's
77
+ * default, and for a model whose thinking defaults ON the reasoning tokens
78
+ * came out of the same output budget as the checkpoint. Resolved through
79
+ * `resolveReasoning` — the one authority the `/model` path uses — so it
80
+ * only ever sends a NATIVE value for a model the registry knows. For a
81
+ * model it does not know (66 of the 73 profiles on the machine this was
82
+ * found on, the failing one among them) it sends nothing, exactly as
83
+ * before: a raw field to an unknown model can be a 400. That is why this
84
+ * is an optimisation and the scaled budget is the fix — the budget
85
+ * reaches every model, this reaches the ones we can vouch for.
86
+ */
87
+ function summaryReasoning(model, baseUrl) {
88
+ const r = resolveReasoning(model, { thinking: "disabled", effort: "default" }, baseUrl);
89
+ return r.ok && r.wire.thinking !== undefined ? { reasoning: r.wire } : {};
90
+ }
72
91
  export class AgentSession {
73
92
  id;
74
93
  log;
@@ -428,6 +447,11 @@ export class AgentSession {
428
447
  // R3a: `focus` — an optional steer for the summary call ("keep the
429
448
  // auth details"). Rides the serialized input as ONE instruction
430
449
  // line; absent = byte-identical to the pre-round call.
450
+ // 0.39.2: `manualBudget` — the MANUAL `/compact` gesture's summary
451
+ // call: the measured output budget, and thinking off where the
452
+ // registry says the model can turn it off. Absent — the auto
453
+ // policy — the call is byte-identical to before; see
454
+ // `MANUAL_SUMMARY_BUDGET` for the measurement and for why.
431
455
  options = {}) {
432
456
  this.ensureHealthy();
433
457
  const keepRounds = options.keepRounds ?? KEEP_RECENT_ROUNDS;
@@ -456,10 +480,11 @@ export class AgentSession {
456
480
  // call with no fraction (kiso never invents a percentage here).
457
481
  if (options.signal !== undefined && options.signal.aborted)
458
482
  throw cancelled();
483
+ const coveredTokens = estimateTokens(covered);
459
484
  options.onStart?.({
460
485
  coversToSeq: boundary,
461
486
  rounds: events.filter((e) => e.type === "user_input" && e.seq > prevPoint && e.seq <= boundary).length,
462
- tokens: estimateTokens(covered),
487
+ tokens: coveredTokens,
463
488
  });
464
489
  let summary;
465
490
  let usage = null;
@@ -483,11 +508,13 @@ export class AgentSession {
483
508
  // E6 (a): ONE serialized user message — the DSML bug's
484
509
  // raw-message array is structurally dead on this path.
485
510
  messages: [{ role: "user", content: serializedInput }],
486
- // E6 (g): the summary call ALWAYS carries the explicit
487
- // output budget — 4,000 adapter maxTokens (a wire-level
488
- // truncation is caught by the (b) required-section
489
- // validation, never silently passed).
490
- maxOutputTokens: SUMMARY_MAX_OUTPUT,
511
+ // E6 (g): the summary call ALWAYS carries an explicit output
512
+ // budget (a wire-level truncation is caught by the (b)
513
+ // required-section validation, never silently passed).
514
+ // 0.39.2: measured for the manual gesture, fixed for the policy.
515
+ ...(options.manualBudget === true
516
+ ? { maxOutputTokens: MANUAL_SUMMARY_BUDGET, ...summaryReasoning(binding.model, binding.baseUrl) }
517
+ : { maxOutputTokens: SUMMARY_MAX_OUTPUT }),
491
518
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
492
519
  });
493
520
  summary = call.text;
@@ -44,7 +44,48 @@ export declare const SUMMARY_RESULT_MAX_CHARS = 2000;
44
44
  export declare const SUMMARY_MAX_OUTPUT = 4000;
45
45
  export declare const KEEP_TOKENS_DEFAULT = 20000;
46
46
  export declare const IN_FLIGHT_HEADROOM = 8000;
47
+ /** ONE NUMBER, TWO CONSUMERS. `SUMMARY_MAX_OUTPUT` is both the summary
48
+ * call's output budget AND a term of this reserve — the reserve buys
49
+ * back room for exactly that much summary. Change one and you have
50
+ * changed the other; see `MANUAL_SUMMARY_BUDGET` for why the manual
51
+ * gesture's larger budget does NOT reach the auto policy that reads this. */
47
52
  export declare const POLICY_RESERVE: number;
53
+ /**
54
+ * 0.39.2 — the output budget of a MANUAL `/compact`, and it is MEASURED.
55
+ *
56
+ * The fixed 4,000 failed live sessions: `the summary turn ended with
57
+ * max_tokens`. On the failing profile (an unregistered DeepSeek model
58
+ * behind a gateway), a covered range the size of the reported one —
59
+ * ~100k tokens of real source — was run three ways:
60
+ *
61
+ * budget 4,000 → max_tokens reasoning 3,093 checkpoint 828 3 of 7 sections
62
+ * budget 8,391 → max_tokens reasoning 1,613 checkpoint 6,544 4 of 7
63
+ * budget 32,000 → end_turn reasoning 3,557 checkpoint 15,261 7 of 7
64
+ *
65
+ * A complete checkpoint took 18,837 output tokens. At 4,000 the reasoning
66
+ * LOOKED like the cause (77% of the budget) only because the budget was
67
+ * too small for the checkpoint to get going; with room, reasoning is a
68
+ * fifth and the checkpoint itself is the size. A rule scaled from the
69
+ * covered estimate (a twelfth: 8,391, doubling to 16,782 on retry) was
70
+ * built first and failed BOTH attempts on this data — it was a guess, and
71
+ * the measurement is what falsified it.
72
+ *
73
+ * So the budget is not scaled; it is a generous flat CAP, because a cap
74
+ * is not a charge — the model is billed for what it writes, and a higher
75
+ * limit costs nothing unless the output would have run away anyway. The
76
+ * one thing a lower first budget buys is a failed call before the one
77
+ * that works. 32,000 holds the measured 18,837 with room, and bounds what
78
+ * a genuinely runaway summary can cost: a checkpoint larger than this has
79
+ * stopped being a summary.
80
+ *
81
+ * MANUAL ONLY, and that is the patch boundary. The auto policy's
82
+ * `POLICY_RESERVE` assumes `SUMMARY_MAX_OUTPUT`; giving the policy a larger
83
+ * budget without moving its reserve would let a fire leave less room than
84
+ * the reserve promised, and moving the reserve moves every session's
85
+ * compaction trigger — BM-1 §3's compaction tier, where the paired bench
86
+ * blocks. That belongs to A1b, and the measurement above is its input.
87
+ */
88
+ export declare const MANUAL_SUMMARY_BUDGET = 32000;
48
89
  /** The reference context-window scale (the flash-family window); the
49
90
  * env overrides. The default arming point is 120,000 − 32,000 =
50
91
  * 88,000 — a post-fire projection (≥ 24k) can never re-cross it, so
@@ -114,6 +155,13 @@ export interface SummarizeConversationOptions {
114
155
  readonly signal?: AbortSignalLike;
115
156
  /** E6 (g): the summary call's explicit output budget (adapter maxTokens). */
116
157
  readonly maxOutputTokens?: number;
158
+ /** 0.39.2: the RESOLVED reasoning for the summary call — native wire
159
+ * values from `resolveReasoning`, never a raw field. Absent sends
160
+ * nothing, and the provider's default applies. */
161
+ readonly reasoning?: {
162
+ readonly thinking?: "adaptive" | "enabled" | "disabled";
163
+ readonly effort?: string;
164
+ };
117
165
  }
118
166
  /** The summary call's result — the text PLUS the provider-reported usage
119
167
  * (E6: the honest accounting — the summary call's cost rides the trace
@@ -124,9 +172,21 @@ export interface SummarizeConversationResult {
124
172
  readonly usage: RawUsage | null;
125
173
  }
126
174
  /**
127
- * The one-shot summary call. Collects the adapter's text deltas into the
128
- * summary; usage/stop pass through untouched. Throws when the model
129
- * produced no text — the caller reports it and nothing is persisted.
175
+ * The summary call, with ONE retry.
176
+ *
177
+ * 0.39.1: `/compact` had no retry at all. The kernel owns retries
178
+ * (ADR-0005) and this path does not go through the kernel — it calls the
179
+ * adapter directly — so a gateway that drops a 95k-token summary stream
180
+ * failed `/compact` every single time, with an honest message and no way
181
+ * forward. The adapter classifies that as a retryable `network` failure
182
+ * (the transport-failure-after-headers class); here is the only place
183
+ * that can act on it.
184
+ *
185
+ * ONE retry, not a loop, and not the kernel's budget: a summary re-pays
186
+ * its whole input, which on the sessions that need compacting is the
187
+ * most expensive request the session makes. A permanent error still
188
+ * throws on the first attempt — the retry is keyed on the classification
189
+ * the adapter already made, never on the fact that something failed.
130
190
  */
131
191
  export declare function summarizeConversation(options: SummarizeConversationOptions): Promise<SummarizeConversationResult>;
132
192
  /** E6 — the crux-experiment drop arm: the covered turns are replaced by
package/dist/summarize.js CHANGED
@@ -41,7 +41,61 @@ export const SUMMARY_RESULT_MAX_CHARS = 2000;
41
41
  export const SUMMARY_MAX_OUTPUT = 4000;
42
42
  export const KEEP_TOKENS_DEFAULT = 20000;
43
43
  export const IN_FLIGHT_HEADROOM = 8000;
44
+ /** ONE NUMBER, TWO CONSUMERS. `SUMMARY_MAX_OUTPUT` is both the summary
45
+ * call's output budget AND a term of this reserve — the reserve buys
46
+ * back room for exactly that much summary. Change one and you have
47
+ * changed the other; see `MANUAL_SUMMARY_BUDGET` for why the manual
48
+ * gesture's larger budget does NOT reach the auto policy that reads this. */
44
49
  export const POLICY_RESERVE = SUMMARY_MAX_OUTPUT + KEEP_TOKENS_DEFAULT + IN_FLIGHT_HEADROOM;
50
+ /**
51
+ * 0.39.2 — the output budget of a MANUAL `/compact`, and it is MEASURED.
52
+ *
53
+ * The fixed 4,000 failed live sessions: `the summary turn ended with
54
+ * max_tokens`. On the failing profile (an unregistered DeepSeek model
55
+ * behind a gateway), a covered range the size of the reported one —
56
+ * ~100k tokens of real source — was run three ways:
57
+ *
58
+ * budget 4,000 → max_tokens reasoning 3,093 checkpoint 828 3 of 7 sections
59
+ * budget 8,391 → max_tokens reasoning 1,613 checkpoint 6,544 4 of 7
60
+ * budget 32,000 → end_turn reasoning 3,557 checkpoint 15,261 7 of 7
61
+ *
62
+ * A complete checkpoint took 18,837 output tokens. At 4,000 the reasoning
63
+ * LOOKED like the cause (77% of the budget) only because the budget was
64
+ * too small for the checkpoint to get going; with room, reasoning is a
65
+ * fifth and the checkpoint itself is the size. A rule scaled from the
66
+ * covered estimate (a twelfth: 8,391, doubling to 16,782 on retry) was
67
+ * built first and failed BOTH attempts on this data — it was a guess, and
68
+ * the measurement is what falsified it.
69
+ *
70
+ * So the budget is not scaled; it is a generous flat CAP, because a cap
71
+ * is not a charge — the model is billed for what it writes, and a higher
72
+ * limit costs nothing unless the output would have run away anyway. The
73
+ * one thing a lower first budget buys is a failed call before the one
74
+ * that works. 32,000 holds the measured 18,837 with room, and bounds what
75
+ * a genuinely runaway summary can cost: a checkpoint larger than this has
76
+ * stopped being a summary.
77
+ *
78
+ * MANUAL ONLY, and that is the patch boundary. The auto policy's
79
+ * `POLICY_RESERVE` assumes `SUMMARY_MAX_OUTPUT`; giving the policy a larger
80
+ * budget without moving its reserve would let a fire leave less room than
81
+ * the reserve promised, and moving the reserve moves every session's
82
+ * compaction trigger — BM-1 §3's compaction tier, where the paired bench
83
+ * blocks. That belongs to A1b, and the measurement above is its input.
84
+ */
85
+ export const MANUAL_SUMMARY_BUDGET = 32_000;
86
+ /** Thrown by an attempt that stopped on `max_tokens`, and it NAMES the
87
+ * budget: "not a complete turn" alone told the person nothing about what
88
+ * ran out. Still an `Error`, and still carries the `max_tokens … not a
89
+ * complete turn` wording the CX-1 F3 gate matches. */
90
+ class SummaryBudgetExhausted extends Error {
91
+ budget;
92
+ constructor(budget) {
93
+ super(budget === undefined
94
+ ? "the summary turn ended with max_tokens — not a complete turn"
95
+ : `the summary turn ended with max_tokens — it needed more than its ${budget}-token output budget, not a complete turn`);
96
+ this.budget = budget;
97
+ }
98
+ }
45
99
  /** The reference context-window scale (the flash-family window); the
46
100
  * env overrides. The default arming point is 120,000 − 32,000 =
47
101
  * 88,000 — a post-fire projection (≥ 24k) can never re-cross it, so
@@ -188,12 +242,74 @@ export function validateSummary(text) {
188
242
  }
189
243
  return null;
190
244
  }
245
+ /** A thrown value the adapter CLASSIFIED — `{ code, retryable, message }`
246
+ * (ADR-0005 rule 1). The core's own guard is private, and this asks a
247
+ * narrower question than that one: is this a failure someone already
248
+ * decided is worth trying again. A validation rejection below is a plain
249
+ * `Error` and can never match, which is the separation E6 (b) wants —
250
+ * "the model wrote a bad summary" is not retried, ever. */
251
+ function retryableStructured(err) {
252
+ if (err === null || typeof err !== "object")
253
+ return false;
254
+ const e = err;
255
+ return typeof e.code === "string" && typeof e.message === "string" && e.retryable === true;
256
+ }
257
+ /** How long to wait before the one retry. The provider's own ask when it
258
+ * made one and it is short enough to be worth honouring; otherwise a
259
+ * token pause — the failure this exists for is a dropped stream, not a
260
+ * rate limit, and an immediate second attempt is the right response to
261
+ * a socket that died. */
262
+ function retryWaitMs(err) {
263
+ const asked = err.retryAfterMs;
264
+ return typeof asked === "number" && Number.isFinite(asked) && asked >= 0 && asked <= 60_000 ? asked : 250;
265
+ }
191
266
  /**
192
- * The one-shot summary call. Collects the adapter's text deltas into the
193
- * summary; usage/stop pass through untouched. Throws when the model
194
- * produced no text — the caller reports it and nothing is persisted.
267
+ * The summary call, with ONE retry.
268
+ *
269
+ * 0.39.1: `/compact` had no retry at all. The kernel owns retries
270
+ * (ADR-0005) and this path does not go through the kernel — it calls the
271
+ * adapter directly — so a gateway that drops a 95k-token summary stream
272
+ * failed `/compact` every single time, with an honest message and no way
273
+ * forward. The adapter classifies that as a retryable `network` failure
274
+ * (the transport-failure-after-headers class); here is the only place
275
+ * that can act on it.
276
+ *
277
+ * ONE retry, not a loop, and not the kernel's budget: a summary re-pays
278
+ * its whole input, which on the sessions that need compacting is the
279
+ * most expensive request the session makes. A permanent error still
280
+ * throws on the first attempt — the retry is keyed on the classification
281
+ * the adapter already made, never on the fact that something failed.
195
282
  */
196
283
  export async function summarizeConversation(options) {
284
+ // Asked as a CALL, twice, because `aborted` is a readonly property and
285
+ // the narrowing from the first read would otherwise be carried across
286
+ // the await — where the whole point is that it can have changed.
287
+ const aborted = () => options.signal?.aborted === true;
288
+ // A `max_tokens` stop is NOT retried here. A retry at the same budget
289
+ // buys the same truncation, and a retry at a larger one is only worth
290
+ // having if the first budget was chosen small — which is why the manual
291
+ // gesture now asks for the whole measured budget up front instead
292
+ // (see `MANUAL_SUMMARY_BUDGET`).
293
+ try {
294
+ return await summaryAttempt(options);
295
+ }
296
+ catch (err) {
297
+ // The caller's cancel is the caller's: an aborted `/compact` must
298
+ // not spend a second call on its way out.
299
+ if (!retryableStructured(err) || aborted())
300
+ throw err;
301
+ await new Promise((r) => setTimeout(r, retryWaitMs(err)));
302
+ if (aborted())
303
+ throw err;
304
+ return await summaryAttempt(options);
305
+ }
306
+ }
307
+ /**
308
+ * ONE attempt. Collects the adapter's text deltas into the summary;
309
+ * usage/stop pass through untouched. Throws when the model produced no
310
+ * text — the caller reports it and nothing is persisted.
311
+ */
312
+ async function summaryAttempt(options) {
197
313
  const { adapter, model, messages } = options;
198
314
  let text = "";
199
315
  let usage = null;
@@ -213,6 +329,7 @@ export async function summarizeConversation(options) {
213
329
  systemPrompt: SUMMARY_PROMPT,
214
330
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
215
331
  ...(options.maxOutputTokens !== undefined ? { maxTokens: options.maxOutputTokens } : {}),
332
+ ...(options.reasoning !== undefined ? { reasoning: options.reasoning } : {}),
216
333
  })) {
217
334
  if (stops > 0 && ev.type !== "usage" && ev.type !== "stop" && afterStop === undefined)
218
335
  afterStop = ev.type;
@@ -247,6 +364,9 @@ export async function summarizeConversation(options) {
247
364
  throw new Error(`the summary turn stopped ${stops} times — not a complete turn`);
248
365
  if (toolCalls > 0)
249
366
  throw new Error("the summary turn called a tool — a summary is text, never a tool call");
367
+ // The one stop a larger budget can cure, told apart from the rest.
368
+ if (stopReason === "max_tokens")
369
+ throw new SummaryBudgetExhausted(options.maxOutputTokens);
250
370
  if (stopReason !== "end_turn")
251
371
  throw new Error(`the summary turn ended with ${String(stopReason)} — not a complete turn`);
252
372
  if (afterStop !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.39.0",
3
+ "version": "0.39.2",
4
4
  "description": "kiso runtime \u2014 durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,12 +25,12 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@vincemakes/kiso-core": "0.39.0"
28
+ "@vincemakes/kiso-core": "0.39.2"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.39.0",
32
- "@vincemakes/kiso-provider-openai": "0.39.0",
33
- "@vincemakes/kiso-provider-openai-responses": "0.39.0"
31
+ "@vincemakes/kiso-provider-anthropic": "0.39.2",
32
+ "@vincemakes/kiso-provider-openai": "0.39.2",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.39.2"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@vincemakes/kiso-provider-anthropic": {
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@vincemakes/kiso-evals": "0.39.0",
47
+ "@vincemakes/kiso-evals": "0.39.2",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"