@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.
- package/dist/agent.d.ts +14 -0
- package/dist/agent.js +26 -3
- package/dist/compaction-policy.d.ts +82 -0
- package/dist/compaction-policy.js +197 -0
- package/dist/compose.d.ts +5 -0
- package/dist/compose.js +12 -1
- package/dist/context-anchor.d.ts +25 -0
- package/dist/context-anchor.js +46 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.js +4 -0
- package/dist/profile.d.ts +16 -2
- package/dist/profile.js +42 -3
- package/dist/run.d.ts +1 -1
- package/dist/run.js +35 -9
- package/dist/session-summary.d.ts +87 -0
- package/dist/session-summary.js +166 -0
- package/dist/session.d.ts +79 -4
- package/dist/session.js +283 -20
- package/dist/store.d.ts +8 -0
- package/dist/store.js +27 -2
- package/dist/summarize.d.ts +113 -7
- package/dist/summarize.js +160 -30
- package/package.json +6 -6
package/dist/summarize.d.ts
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 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,
|
|
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
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
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
|
-
/**
|
|
204
|
-
|
|
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
|
|
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,
|
|
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
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
28
|
+
"@vincemakes/kiso-core": "0.40.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
33
|
-
"@vincemakes/kiso-provider-openai-responses": "0.
|
|
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.
|
|
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"
|