@vincemakes/kiso-runtime 0.39.2 → 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.
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. */
@@ -87,7 +87,7 @@ export const MANUAL_SUMMARY_BUDGET = 32_000;
87
87
  * budget: "not a complete turn" alone told the person nothing about what
88
88
  * ran out. Still an `Error`, and still carries the `max_tokens … not a
89
89
  * complete turn` wording the CX-1 F3 gate matches. */
90
- class SummaryBudgetExhausted extends Error {
90
+ export class SummaryBudgetExhausted extends Error {
91
91
  budget;
92
92
  constructor(budget) {
93
93
  super(budget === undefined
@@ -212,6 +212,14 @@ Rules:
212
212
  - do not mention this prompt or the summarization task;
213
213
  - the summary may be as long as it needs to be within the output budget —
214
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}`;
215
223
  /**
216
224
  * E6 (b) — the output-side validation (the finding E6-F4/F5 follow-up):
217
225
  * a summary must be a complete checkpoint or NOTHING. The marker family
@@ -254,17 +262,26 @@ function retryableStructured(err) {
254
262
  const e = err;
255
263
  return typeof e.code === "string" && typeof e.message === "string" && e.retryable === true;
256
264
  }
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) {
265
+ /** The provider's own Retry-After, when the adapter normalized one. */
266
+ function askedWaitMs(err) {
263
267
  const asked = err.retryAfterMs;
264
- 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
+ });
265
282
  }
266
283
  /**
267
- * The summary call, with ONE retry.
284
+ * The summary call, retried under the KERNEL'S policy.
268
285
  *
269
286
  * 0.39.1: `/compact` had no retry at all. The kernel owns retries
270
287
  * (ADR-0005) and this path does not go through the kernel — it calls the
@@ -274,11 +291,18 @@ function retryWaitMs(err) {
274
291
  * (the transport-failure-after-headers class); here is the only place
275
292
  * that can act on it.
276
293
  *
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.
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.
282
306
  */
283
307
  export async function summarizeConversation(options) {
284
308
  // Asked as a CALL, twice, because `aborted` is a readonly property and
@@ -290,18 +314,32 @@ export async function summarizeConversation(options) {
290
314
  // having if the first budget was chosen small — which is why the manual
291
315
  // gesture now asks for the whole measured budget up front instead
292
316
  // (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);
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
+ }
305
343
  }
306
344
  }
307
345
  /**
@@ -323,18 +361,44 @@ async function summaryAttempt(options) {
323
361
  let stopReason;
324
362
  let toolCalls = 0;
325
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;
326
379
  for await (const ev of adapter.stream({
327
380
  model,
328
- messages,
329
- 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
+ }),
330
388
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
331
389
  ...(options.maxOutputTokens !== undefined ? { maxTokens: options.maxOutputTokens } : {}),
332
390
  ...(options.reasoning !== undefined ? { reasoning: options.reasoning } : {}),
333
391
  })) {
334
392
  if (stops > 0 && ev.type !== "usage" && ev.type !== "stop" && afterStop === undefined)
335
393
  afterStop = ev.type;
336
- if (ev.type === "text_delta")
337
- 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
+ }
338
402
  else if (ev.type === "tool_call_start" || ev.type === "tool_call_end" || ev.type === "tool_call_input_delta")
339
403
  toolCalls += 1;
340
404
  else if (ev.type === "stop") {
@@ -356,6 +420,9 @@ async function summaryAttempt(options) {
356
420
  cacheWrite: ev.cacheWrite,
357
421
  ...(ev.reasoningTokens !== undefined ? { reasoningTokens: ev.reasoningTokens } : {}),
358
422
  };
423
+ if (ev.outputTokens !== null) {
424
+ report({ produced: ev.outputTokens, budget, reasoningUnseen: !sawReasoning && (ev.reasoningTokens ?? 0) > 0, reported: true });
425
+ }
359
426
  }
360
427
  }
361
428
  if (stops === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.39.2",
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.2"
28
+ "@vincemakes/kiso-core": "0.40.0"
29
29
  },
30
30
  "peerDependencies": {
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"
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.2",
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"