@vincemakes/kiso-runtime 0.39.1 → 0.40.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.
@@ -13,7 +13,7 @@
13
13
  * unchanged ("nothing happened"). Only the generated `summarized` event
14
14
  * lands on disk; the original events stay there forever.
15
15
  */
16
- import type { AbortSignalLike, Adapter } from "@vincemakes/kiso-core";
16
+ import type { AbortSignalLike, Adapter, RetryInfo, ToolSpec } from "@vincemakes/kiso-core";
17
17
  import type { Event } from "@vincemakes/kiso-core";
18
18
  import type { Message } from "@vincemakes/kiso-core";
19
19
  import type { RawUsage } from "./usage/canonical.js";
@@ -44,7 +44,56 @@ 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;
89
+ /** Thrown by an attempt that stopped on `max_tokens`, and it NAMES the
90
+ * budget: "not a complete turn" alone told the person nothing about what
91
+ * ran out. Still an `Error`, and still carries the `max_tokens … not a
92
+ * complete turn` wording the CX-1 F3 gate matches. */
93
+ export declare class SummaryBudgetExhausted extends Error {
94
+ readonly budget: number | undefined;
95
+ constructor(budget: number | undefined);
96
+ }
48
97
  /** The reference context-window scale (the flash-family window); the
49
98
  * env overrides. The default arming point is 120,000 − 32,000 =
50
99
  * 88,000 — a post-fire projection (≥ 24k) can never re-cross it, so
@@ -90,6 +139,12 @@ export declare function serializeCovered(options: SerializeCoveredOptions): stri
90
139
  * (the loop's system prompt is the harness's business, never the kernel's).
91
140
  */
92
141
  export declare const SUMMARY_PROMPT = "Only output the summary. Do not continue the conversation. Do not use any tools.\n\nYou are the conversation summarizer of the kiso agent framework.\n\nSummarize the covered conversation into a single structured checkpoint\nthat will REPLACE it in the model's context. The next turn must be able\nto continue the work without reading the originals.\n\nProduce the checkpoint with exactly these sections, in this order:\n\n## Goal\nThe user's goal and the acceptance criterion, in one or two sentences.\n\n## Constraints\nThe constraints, requirements, and rulings the work must honor.\n\n## User requests\nEvery user message in the covered range, enumerated one by one, each\nwith what it asked for and what was done about it.\n\n## Files and changes\nEvery file touched \u2014 exact paths, what changed, and why. Include the\nprecise code-level changes later turns may need to continue.\n\n## Errors and fixes\nEvery error encountered and its resolution; commands run and their\noutcomes.\n\n## Current work\nThe current state of the work \u2014 what is done, what is not. Quote the\ncurrent task's criterion VERBATIM if one exists.\n\n## Next steps\nThe concrete next steps, in order.\n\nPreserve concrete identifiers VERBATIM: paths, function names, task ids,\nenvironment names \u2014 never paraphrase them.\n\nRules:\n- plain prose \u2014 no bullet lists, no markdown outside the section headers,\n no prefixes;\n- do not mention this prompt or the summarization task;\n- the summary may be as long as it needs to be within the output budget \u2014\n there is no word cap; completeness wins.";
142
+ /**
143
+ * ADR-0055 A2 — the IN-BAND form: the run's own request with this appended
144
+ * as the last user message, so everything before it reads at the cache-hit
145
+ * price. The same sections, the same validation; the guard sentence first.
146
+ */
147
+ export declare const SUMMARY_IN_BAND = "Stop the task for this one reply and write a checkpoint of the conversation above instead.\n\nOnly output the summary. Do not continue the conversation. Do not use any tools.\n\nYou are the conversation summarizer of the kiso agent framework.\n\nSummarize the covered conversation into a single structured checkpoint\nthat will REPLACE it in the model's context. The next turn must be able\nto continue the work without reading the originals.\n\nProduce the checkpoint with exactly these sections, in this order:\n\n## Goal\nThe user's goal and the acceptance criterion, in one or two sentences.\n\n## Constraints\nThe constraints, requirements, and rulings the work must honor.\n\n## User requests\nEvery user message in the covered range, enumerated one by one, each\nwith what it asked for and what was done about it.\n\n## Files and changes\nEvery file touched \u2014 exact paths, what changed, and why. Include the\nprecise code-level changes later turns may need to continue.\n\n## Errors and fixes\nEvery error encountered and its resolution; commands run and their\noutcomes.\n\n## Current work\nThe current state of the work \u2014 what is done, what is not. Quote the\ncurrent task's criterion VERBATIM if one exists.\n\n## Next steps\nThe concrete next steps, in order.\n\nPreserve concrete identifiers VERBATIM: paths, function names, task ids,\nenvironment names \u2014 never paraphrase them.\n\nRules:\n- plain prose \u2014 no bullet lists, no markdown outside the section headers,\n no prefixes;\n- do not mention this prompt or the summarization task;\n- the summary may be as long as it needs to be within the output budget \u2014\n there is no word cap; completeness wins.";
93
148
  /**
94
149
  * E6 (b) — the output-side validation (the finding E6-F4/F5 follow-up):
95
150
  * a summary must be a complete checkpoint or NOTHING. The marker family
@@ -109,11 +164,55 @@ export declare function validateSummary(text: string): string | null;
109
164
  export interface SummarizeConversationOptions {
110
165
  readonly adapter: Adapter;
111
166
  readonly model: string;
167
+ /** ADR-0055 A2: send `messages` AS THE RUN SENT THEM, under the run's own
168
+ * system prompt and tool table, with SUMMARY_IN_BAND appended — the
169
+ * cache-hot form. Absent: `messages` is the serialized covered range
170
+ * under SUMMARY_PROMPT, the cold form. */
171
+ readonly inBand?: {
172
+ readonly systemPrompt?: string;
173
+ readonly tools: readonly ToolSpec[];
174
+ readonly focus?: string;
175
+ };
112
176
  /** The covered conversation — the ONLY material the summary is about. */
113
177
  readonly messages: readonly Message[];
114
178
  readonly signal?: AbortSignalLike;
115
179
  /** E6 (g): the summary call's explicit output budget (adapter maxTokens). */
116
180
  readonly maxOutputTokens?: number;
181
+ /** 0.39.2: the RESOLVED reasoning for the summary call — native wire
182
+ * values from `resolveReasoning`, never a raw field. Absent sends
183
+ * nothing, and the provider's default applies. */
184
+ readonly reasoning?: {
185
+ readonly thinking?: "adaptive" | "enabled" | "disabled";
186
+ readonly effort?: string;
187
+ };
188
+ /** ADR-0005 Amendment 2: the kernel's retry budget, applied to this
189
+ * off-loop call. Absent, the kernel's default. */
190
+ readonly maxRetries?: number;
191
+ /** ADR-0005 Amendment 2: announced before each wait, as the kernel
192
+ * announces its own. Observation only — a throw is swallowed. */
193
+ readonly onRetry?: (info: RetryInfo) => Promise<void> | void;
194
+ /** 0.40.0: how much of the OUTPUT budget the call has spent so far —
195
+ * reported at each attempt's start (zero) and as text, reasoning and
196
+ * the final usage arrive. Observation only: a throw is swallowed, and
197
+ * the request is identical with or without it. */
198
+ readonly onProgress?: (progress: SummaryProgress) => void;
199
+ }
200
+ /**
201
+ * 0.40.0 — the summary call's progress against its output budget.
202
+ *
203
+ * `produced` counts streamed text AND streamed reasoning (owner's ruling —
204
+ * a bar over text alone sits low while thinking spends the budget, and the
205
+ * call then fails at "full"), by the session's chars/4 proxy, until the
206
+ * provider reports its output total, which replaces the estimate
207
+ * (`reported`). `reasoningUnseen`: the provider billed reasoning that never
208
+ * streamed, so the jump to the reported total has a named cause.
209
+ */
210
+ export interface SummaryProgress {
211
+ readonly produced: number;
212
+ /** the call's output budget; null when the call carries none */
213
+ readonly budget: number | null;
214
+ readonly reasoningUnseen: boolean;
215
+ readonly reported: boolean;
117
216
  }
118
217
  /** The summary call's result — the text PLUS the provider-reported usage
119
218
  * (E6: the honest accounting — the summary call's cost rides the trace
@@ -124,7 +223,7 @@ export interface SummarizeConversationResult {
124
223
  readonly usage: RawUsage | null;
125
224
  }
126
225
  /**
127
- * The summary call, with ONE retry.
226
+ * The summary call, retried under the KERNEL'S policy.
128
227
  *
129
228
  * 0.39.1: `/compact` had no retry at all. The kernel owns retries
130
229
  * (ADR-0005) and this path does not go through the kernel — it calls the
@@ -134,11 +233,18 @@ export interface SummarizeConversationResult {
134
233
  * (the transport-failure-after-headers class); here is the only place
135
234
  * that can act on it.
136
235
  *
137
- * ONE retry, not a loop, and not the kernel's budget: a summary re-pays
138
- * its whole input, which on the sessions that need compacting is the
139
- * most expensive request the session makes. A permanent error still
140
- * throws on the first attempt — the retry is keyed on the classification
141
- * the adapter already made, never on the fact that something failed.
236
+ * 0.39.1 gave it ONE retry after 250 ms. ADR-0005 Amendment 2 gives it the
237
+ * kernel's rule instead, because a dropped summary stream is the same
238
+ * failure as a dropped turn and meets the same gateway: the same curve,
239
+ * the same budget (the same `maxRetries` knob), Retry-After as a floor and
240
+ * a wait beyond RETRY_AFTER_MAX_MS as an explicit stop — never a retry
241
+ * that comes early — and the same announcement before each wait. Each
242
+ * retry re-pays the summary's input, exactly as each turn retry re-pays
243
+ * the turn's; the budget that bounds one bounds the other.
244
+ *
245
+ * A permanent error still throws on the first attempt — the retry is
246
+ * keyed on the classification the adapter already made, never on the
247
+ * fact that something failed.
142
248
  */
143
249
  export declare function summarizeConversation(options: SummarizeConversationOptions): Promise<SummarizeConversationResult>;
144
250
  /** E6 — the crux-experiment drop arm: the covered turns are replaced by
package/dist/summarize.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * unchanged ("nothing happened"). Only the generated `summarized` event
14
14
  * lands on disk; the original events stay there forever.
15
15
  */
16
- import { estimateTokens, DO_NOT_COMPACT } from "@vincemakes/kiso-core";
16
+ import { estimateTokens, DO_NOT_COMPACT, DEFAULT_MAX_RETRIES, RETRY_AFTER_MAX_MS, retryDelayMs } from "@vincemakes/kiso-core";
17
17
  /** K (ADR-0044): the recent ROUNDS kept intact by /compact — a constant,
18
18
  * not a knob. The covered range ends just before the K-th most recent
19
19
  * round, so the model still reasons over the recent conversation. */
@@ -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
+ export 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
@@ -158,6 +212,14 @@ Rules:
158
212
  - do not mention this prompt or the summarization task;
159
213
  - the summary may be as long as it needs to be within the output budget —
160
214
  there is no word cap; completeness wins.`;
215
+ /**
216
+ * ADR-0055 A2 — the IN-BAND form: the run's own request with this appended
217
+ * as the last user message, so everything before it reads at the cache-hit
218
+ * price. The same sections, the same validation; the guard sentence first.
219
+ */
220
+ export const SUMMARY_IN_BAND = `Stop the task for this one reply and write a checkpoint of the conversation above instead.
221
+
222
+ ${SUMMARY_PROMPT}`;
161
223
  /**
162
224
  * E6 (b) — the output-side validation (the finding E6-F4/F5 follow-up):
163
225
  * a summary must be a complete checkpoint or NOTHING. The marker family
@@ -200,17 +262,26 @@ function retryableStructured(err) {
200
262
  const e = err;
201
263
  return typeof e.code === "string" && typeof e.message === "string" && e.retryable === true;
202
264
  }
203
- /** How long to wait before the one retry. The provider's own ask when it
204
- * made one and it is short enough to be worth honouring; otherwise a
205
- * token pause — the failure this exists for is a dropped stream, not a
206
- * rate limit, and an immediate second attempt is the right response to
207
- * a socket that died. */
208
- function retryWaitMs(err) {
265
+ /** The provider's own Retry-After, when the adapter normalized one. */
266
+ function askedWaitMs(err) {
209
267
  const asked = err.retryAfterMs;
210
- return typeof asked === "number" && Number.isFinite(asked) && asked >= 0 && asked <= 60_000 ? asked : 250;
268
+ return typeof asked === "number" && Number.isFinite(asked) && asked >= 0 ? asked : undefined;
269
+ }
270
+ /** The kernel's abortable wait, restated for the one call that is not the
271
+ * kernel's: a cancel during the backoff wakes it at once. */
272
+ function wait(ms, signal) {
273
+ return new Promise((resolve) => {
274
+ if (signal?.aborted === true)
275
+ return resolve();
276
+ const timer = setTimeout(resolve, ms);
277
+ signal?.addEventListener?.("abort", () => {
278
+ clearTimeout(timer);
279
+ resolve();
280
+ }, { once: true });
281
+ });
211
282
  }
212
283
  /**
213
- * The summary call, with ONE retry.
284
+ * The summary call, retried under the KERNEL'S policy.
214
285
  *
215
286
  * 0.39.1: `/compact` had no retry at all. The kernel owns retries
216
287
  * (ADR-0005) and this path does not go through the kernel — it calls the
@@ -220,29 +291,55 @@ function retryWaitMs(err) {
220
291
  * (the transport-failure-after-headers class); here is the only place
221
292
  * that can act on it.
222
293
  *
223
- * ONE retry, not a loop, and not the kernel's budget: a summary re-pays
224
- * its whole input, which on the sessions that need compacting is the
225
- * most expensive request the session makes. A permanent error still
226
- * throws on the first attempt — the retry is keyed on the classification
227
- * the adapter already made, never on the fact that something failed.
294
+ * 0.39.1 gave it ONE retry after 250 ms. ADR-0005 Amendment 2 gives it the
295
+ * kernel's rule instead, because a dropped summary stream is the same
296
+ * failure as a dropped turn and meets the same gateway: the same curve,
297
+ * the same budget (the same `maxRetries` knob), Retry-After as a floor and
298
+ * a wait beyond RETRY_AFTER_MAX_MS as an explicit stop — never a retry
299
+ * that comes early — and the same announcement before each wait. Each
300
+ * retry re-pays the summary's input, exactly as each turn retry re-pays
301
+ * the turn's; the budget that bounds one bounds the other.
302
+ *
303
+ * A permanent error still throws on the first attempt — the retry is
304
+ * keyed on the classification the adapter already made, never on the
305
+ * fact that something failed.
228
306
  */
229
307
  export async function summarizeConversation(options) {
230
308
  // Asked as a CALL, twice, because `aborted` is a readonly property and
231
309
  // the narrowing from the first read would otherwise be carried across
232
310
  // the await — where the whole point is that it can have changed.
233
311
  const aborted = () => options.signal?.aborted === true;
234
- try {
235
- return await summaryAttempt(options);
236
- }
237
- catch (err) {
238
- // The caller's cancel is the caller's: an aborted `/compact` must
239
- // not spend a second call on its way out.
240
- if (!retryableStructured(err) || aborted())
241
- throw err;
242
- await new Promise((r) => setTimeout(r, retryWaitMs(err)));
243
- if (aborted())
244
- throw err;
245
- return await summaryAttempt(options);
312
+ // A `max_tokens` stop is NOT retried here. A retry at the same budget
313
+ // buys the same truncation, and a retry at a larger one is only worth
314
+ // having if the first budget was chosen small — which is why the manual
315
+ // gesture now asks for the whole measured budget up front instead
316
+ // (see `MANUAL_SUMMARY_BUDGET`).
317
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
318
+ for (let attempts = 0;; attempts += 1) {
319
+ try {
320
+ return await summaryAttempt(options);
321
+ }
322
+ catch (err) {
323
+ // The caller's cancel is the caller's: an aborted `/compact` must
324
+ // not spend another call on its way out.
325
+ if (!retryableStructured(err) || aborted() || attempts >= maxRetries)
326
+ throw err;
327
+ const asked = askedWaitMs(err);
328
+ const delay = retryDelayMs(attempts + 1, asked);
329
+ const e = err;
330
+ if (delay > RETRY_AFTER_MAX_MS) {
331
+ throw { ...e, retryable: false, message: `${e.message} (the provider asked to wait ${asked}ms — beyond the ${RETRY_AFTER_MAX_MS}ms cap; not retried)` };
332
+ }
333
+ try {
334
+ await options.onRetry?.({ attempt: attempts + 1, maxRetries, code: e.code, delayMs: delay, midStream: false });
335
+ }
336
+ catch {
337
+ // observation only
338
+ }
339
+ await wait(delay, options.signal);
340
+ if (aborted())
341
+ throw err;
342
+ }
246
343
  }
247
344
  }
248
345
  /**
@@ -264,17 +361,44 @@ async function summaryAttempt(options) {
264
361
  let stopReason;
265
362
  let toolCalls = 0;
266
363
  let afterStop;
364
+ // 0.40.0: the progress this attempt reports — it starts at zero, so a
365
+ // retry visibly starts the bar again
366
+ let streamedChars = 0;
367
+ let sawReasoning = false;
368
+ const report = (progress) => {
369
+ try {
370
+ options.onProgress?.(progress);
371
+ }
372
+ catch {
373
+ // observation only
374
+ }
375
+ };
376
+ const budget = options.maxOutputTokens ?? null;
377
+ report({ produced: 0, budget, reasoningUnseen: false, reported: false });
378
+ const inBand = options.inBand;
267
379
  for await (const ev of adapter.stream({
268
380
  model,
269
- messages,
270
- systemPrompt: SUMMARY_PROMPT,
381
+ ...(inBand === undefined
382
+ ? { messages, systemPrompt: SUMMARY_PROMPT }
383
+ : {
384
+ messages: [...messages, { role: "user", content: inBand.focus === undefined ? SUMMARY_IN_BAND : `${SUMMARY_IN_BAND}\n\nFocus the summary on: ${inBand.focus}` }],
385
+ ...(inBand.systemPrompt !== undefined ? { systemPrompt: inBand.systemPrompt } : {}),
386
+ tools: inBand.tools,
387
+ }),
271
388
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
272
389
  ...(options.maxOutputTokens !== undefined ? { maxTokens: options.maxOutputTokens } : {}),
390
+ ...(options.reasoning !== undefined ? { reasoning: options.reasoning } : {}),
273
391
  })) {
274
392
  if (stops > 0 && ev.type !== "usage" && ev.type !== "stop" && afterStop === undefined)
275
393
  afterStop = ev.type;
276
- if (ev.type === "text_delta")
277
- text += ev.text;
394
+ if (ev.type === "text_delta" || ev.type === "thinking") {
395
+ if (ev.type === "text_delta")
396
+ text += ev.text;
397
+ else
398
+ sawReasoning = true;
399
+ streamedChars += ev.text.length;
400
+ report({ produced: Math.ceil(streamedChars / 4), budget, reasoningUnseen: false, reported: false });
401
+ }
278
402
  else if (ev.type === "tool_call_start" || ev.type === "tool_call_end" || ev.type === "tool_call_input_delta")
279
403
  toolCalls += 1;
280
404
  else if (ev.type === "stop") {
@@ -296,6 +420,9 @@ async function summaryAttempt(options) {
296
420
  cacheWrite: ev.cacheWrite,
297
421
  ...(ev.reasoningTokens !== undefined ? { reasoningTokens: ev.reasoningTokens } : {}),
298
422
  };
423
+ if (ev.outputTokens !== null) {
424
+ report({ produced: ev.outputTokens, budget, reasoningUnseen: !sawReasoning && (ev.reasoningTokens ?? 0) > 0, reported: true });
425
+ }
299
426
  }
300
427
  }
301
428
  if (stops === 0)
@@ -304,6 +431,9 @@ async function summaryAttempt(options) {
304
431
  throw new Error(`the summary turn stopped ${stops} times — not a complete turn`);
305
432
  if (toolCalls > 0)
306
433
  throw new Error("the summary turn called a tool — a summary is text, never a tool call");
434
+ // The one stop a larger budget can cure, told apart from the rest.
435
+ if (stopReason === "max_tokens")
436
+ throw new SummaryBudgetExhausted(options.maxOutputTokens);
307
437
  if (stopReason !== "end_turn")
308
438
  throw new Error(`the summary turn ended with ${String(stopReason)} — not a complete turn`);
309
439
  if (afterStop !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.39.1",
3
+ "version": "0.40.0",
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.1"
28
+ "@vincemakes/kiso-core": "0.40.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.39.1",
32
- "@vincemakes/kiso-provider-openai": "0.39.1",
33
- "@vincemakes/kiso-provider-openai-responses": "0.39.1"
31
+ "@vincemakes/kiso-provider-anthropic": "0.40.0",
32
+ "@vincemakes/kiso-provider-openai": "0.40.0",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.40.0"
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.1",
47
+ "@vincemakes/kiso-evals": "0.40.0",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"