@genesislcap/ai-assistant 15.23.0 → 15.23.1-FUI-0.4
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/ai-assistant.api.json +269 -5
- package/dist/ai-assistant.d.ts +256 -31
- package/dist/chat-driver.cjs +125 -58
- package/dist/chat-driver.cjs.map +2 -2
- package/dist/chat-driver.mjs +125 -58
- package/dist/chat-driver.mjs.map +2 -2
- package/dist/custom-elements.json +125 -6
- package/dist/dts/components/ai-driver/ai-driver.d.ts +63 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +53 -17
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +16 -1
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +102 -14
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/utils/context-tokens.d.ts +100 -43
- package/dist/dts/utils/context-tokens.d.ts.map +1 -1
- package/dist/dts/utils/history-transform.d.ts +7 -1
- package/dist/dts/utils/history-transform.d.ts.map +1 -1
- package/dist/dts/utils/resolve-context-budget.d.ts +8 -5
- package/dist/dts/utils/resolve-context-budget.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +22 -2
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +92 -0
- package/dist/esm/config/define-stateful-agent.js +1 -0
- package/dist/esm/main/main.js +170 -27
- package/dist/esm/utils/context-tokens.js +331 -106
- package/dist/esm/utils/history-transform.js +5 -5
- package/dist/esm/utils/resolve-context-budget.js +37 -7
- package/package.json +16 -16
|
@@ -26,6 +26,90 @@ export const APPROX_CHARS_PER_TOKEN = 4;
|
|
|
26
26
|
const IMAGE_TOKEN_ESTIMATE = 1600;
|
|
27
27
|
/** Per-message role/framing overhead every transport adds around the content. */
|
|
28
28
|
const PER_MESSAGE_FRAMING_TOKENS = 4;
|
|
29
|
+
/**
|
|
30
|
+
* Chars-per-token for the system prompt — prose, so the ordinary ratio applies.
|
|
31
|
+
*/
|
|
32
|
+
const PREAMBLE_PROSE_CHARS_PER_TOKEN = 4;
|
|
33
|
+
/**
|
|
34
|
+
* Chars-per-token for the tool definitions — JSON, which tokenizes WORSE than
|
|
35
|
+
* prose. Punctuation-dense, short quoted keys, deep nesting: `{`, `"`, `:`, `[`
|
|
36
|
+
* mostly cost a token each where four characters of prose share one.
|
|
37
|
+
*
|
|
38
|
+
* **This ratio is REASONED, not measured** — see {@link estimatePreambleTokens}
|
|
39
|
+
* for exactly what was measured and what was not. It is deliberately the
|
|
40
|
+
* pessimistic side of the plausible 3–3.5 range: this figure feeds the guard that
|
|
41
|
+
* refuses an oversized request, and over-stating the preamble costs a little
|
|
42
|
+
* unused headroom where under-stating lets a turn overflow.
|
|
43
|
+
*/
|
|
44
|
+
const PREAMBLE_JSON_CHARS_PER_TOKEN = 3;
|
|
45
|
+
/**
|
|
46
|
+
* Per-tool framing allowance, the tool-surface equivalent of
|
|
47
|
+
* {@link PER_MESSAGE_FRAMING_TOKENS}.
|
|
48
|
+
*
|
|
49
|
+
* What reaches the wire is not the JSON we serialize: every transport reshapes a
|
|
50
|
+
* `ChatToolDefinition` into its own vendor form (`input_schema` for Anthropic,
|
|
51
|
+
* `functionDeclarations` for Gemini) and wraps each one. Counting only the
|
|
52
|
+
* serialized bytes therefore misses a fixed cost per tool, which on an agent
|
|
53
|
+
* advertising a large surface is not negligible.
|
|
54
|
+
*/
|
|
55
|
+
const PER_TOOL_FRAMING_TOKENS = 8;
|
|
56
|
+
/**
|
|
57
|
+
* Token cost of everything a request carries that is NOT a message — the system
|
|
58
|
+
* prompt, the tool definitions, and the framed tail (GENC-1582).
|
|
59
|
+
*
|
|
60
|
+
* **Why this is measured per call rather than derived.** The preamble used to be
|
|
61
|
+
* inferred as a residual: the first measured prompt minus the char estimate of the
|
|
62
|
+
* messages it covered. That derivation is accurate at the time, but it is taken
|
|
63
|
+
* ONCE and never updated, while the preamble changes every time the agent changes
|
|
64
|
+
* — each agent brings its own system prompt and its own tool surface. Every
|
|
65
|
+
* attempt to patch the staleness downstream made it worse; the cap that used to
|
|
66
|
+
* sit in `calibrate` compared stored CHARACTERS against a wire-measured TOKEN
|
|
67
|
+
* total and collapsed the preamble to zero on any condensed session, which is the
|
|
68
|
+
* same category error this module exists to fix. Measuring what is actually being
|
|
69
|
+
* sent removes the whole class of problem.
|
|
70
|
+
*
|
|
71
|
+
* **What is measured and what is not.** From the isa-platform session of
|
|
72
|
+
* 2026-09-03 (project 067f0fca, `claude-sonnet-5`), the first anchor measured
|
|
73
|
+
* 5,249 tokens against a single 290-character user message, so:
|
|
74
|
+
*
|
|
75
|
+
* | quantity | value | how |
|
|
76
|
+
* |---|---|---|
|
|
77
|
+
* | whole preamble, Bootstrap's first call | **5,173 tok** | MEASURED (5,249 − ~76 for the message) |
|
|
78
|
+
* | system prompt | **8,387 chars** | MEASURED (diag `turn` records it verbatim) |
|
|
79
|
+
* | system prompt in tokens | 2,097 | prose ratio applied to the measured chars |
|
|
80
|
+
* | 3 tool definitions | 3,076 tok (~1,025/tool) | **INFERRED as the residual** |
|
|
81
|
+
*
|
|
82
|
+
* The tools figure is a residual in TOKENS, not a ratio: diagnostics record
|
|
83
|
+
* `toolNames` only, never the schemas, so their character count is unavailable and
|
|
84
|
+
* `PREAMBLE_JSON_CHARS_PER_TOKEN` could not be calibrated from it. It is consistent
|
|
85
|
+
* with 3 — ~1,025 tokens per tool implies ~3,075 characters of schema per tool,
|
|
86
|
+
* which is the right order for a Create tool carrying descriptions and enum
|
|
87
|
+
* options — but consistent is not measured, and the next person should not read it
|
|
88
|
+
* as such. To calibrate it properly, log serialized schema characters alongside the
|
|
89
|
+
* first anchor for one real session and solve for the ratio.
|
|
90
|
+
*
|
|
91
|
+
* `tailContext` counts. It is regenerated per turn rather than accumulated, so
|
|
92
|
+
* compaction cannot remove it any more than it can remove the system prompt, which
|
|
93
|
+
* is the property that matters to every consumer of this figure.
|
|
94
|
+
*/
|
|
95
|
+
export function estimatePreambleTokens(input) {
|
|
96
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
97
|
+
const prose = ((_b = (_a = input.systemPrompt) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) + ((_d = (_c = input.tailContext) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0);
|
|
98
|
+
let json = 0;
|
|
99
|
+
for (const def of (_e = input.toolDefinitions) !== null && _e !== void 0 ? _e : []) {
|
|
100
|
+
try {
|
|
101
|
+
json += JSON.stringify(def).length;
|
|
102
|
+
}
|
|
103
|
+
catch (_k) {
|
|
104
|
+
// Circular or otherwise unserializable: the framing allowance below still
|
|
105
|
+
// charges for the tool itself rather than dropping it silently.
|
|
106
|
+
json += (_g = (_f = def.name) === null || _f === void 0 ? void 0 : _f.length) !== null && _g !== void 0 ? _g : 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return (Math.ceil(prose / PREAMBLE_PROSE_CHARS_PER_TOKEN) +
|
|
110
|
+
Math.ceil(json / PREAMBLE_JSON_CHARS_PER_TOKEN) +
|
|
111
|
+
((_j = (_h = input.toolDefinitions) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0) * PER_TOOL_FRAMING_TOKENS);
|
|
112
|
+
}
|
|
29
113
|
/**
|
|
30
114
|
* Allowance for the summary a compaction will produce, used when projecting the
|
|
31
115
|
* post-compaction size before the summarizer has run. The system prompt asks for
|
|
@@ -174,6 +258,190 @@ function usableAnchor(m, compactedAt) {
|
|
|
174
258
|
function firstAnchor(history, compactedAt) {
|
|
175
259
|
return history.findIndex((m) => usableAnchor(m, compactedAt));
|
|
176
260
|
}
|
|
261
|
+
/** Index of the LAST message carrying a usable measurement, or `-1`. */
|
|
262
|
+
function lastAnchor(history, compactedAt) {
|
|
263
|
+
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
264
|
+
if (usableAnchor(history[i], compactedAt))
|
|
265
|
+
return i;
|
|
266
|
+
}
|
|
267
|
+
return -1;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* The one calibration every estimate in this module is derived from (GENC-1582).
|
|
271
|
+
*
|
|
272
|
+
* **What `inputTokens` actually measures, and what it does not.** It is the size
|
|
273
|
+
* of the prompt a call really SENT — that is, the prompt after `condenseWhen` has
|
|
274
|
+
* stubbed spent tool payloads and per-agent masking has blanked another agent's.
|
|
275
|
+
* It is therefore an authoritative measurement of the wire, and no measurement of
|
|
276
|
+
* stored history exists at all. The first implementation of this module had that
|
|
277
|
+
* backwards (see the note left on {@link estimateRequestTokens}) and treated the
|
|
278
|
+
* anchors as describing untransformed history, which is where every figure here
|
|
279
|
+
* went wrong.
|
|
280
|
+
*
|
|
281
|
+
* **Why the old segment chain could not be repaired in place.** It charged each
|
|
282
|
+
* pair of consecutive anchors the DIFFERENCE between their measurements and kept
|
|
283
|
+
* the result, skipping the pair when the difference was negative. Growth was
|
|
284
|
+
* banked permanently; shrinkage was discarded. The total was consequently an
|
|
285
|
+
* integral of everything the conversation had ever grown by and rose
|
|
286
|
+
* monotonically, so no condensation, masking or compaction could ever bring it
|
|
287
|
+
* down. On a measured Create session it read 868,761 tokens — 87% of a 1M window
|
|
288
|
+
* — against a real last prompt of 107,928, having accumulated 857,712 of growth
|
|
289
|
+
* while throwing away 755,033 of shrinkage. Since the same figure gates the
|
|
290
|
+
* composer and, through `estimateRequestTokens`, the running-turn guard, it was
|
|
291
|
+
* on course to refuse work on a conversation using 11% of its window.
|
|
292
|
+
*
|
|
293
|
+
* **The model that replaced it.** Three facts, in the order they can be known:
|
|
294
|
+
*
|
|
295
|
+
* 1. The FIRST measurement is the only place the preamble is separable: it covers
|
|
296
|
+
* the preamble plus a handful of messages, so subtracting the char estimate of
|
|
297
|
+
* those messages leaves the preamble (see {@link estimateSystemOverhead}).
|
|
298
|
+
* 2. The LAST measurement is authoritative for everything before it — it is the
|
|
299
|
+
* most recent true statement about the size of this conversation — so the total
|
|
300
|
+
* is that figure plus the char estimate of whatever has been appended since.
|
|
301
|
+
* Nothing is chained, so nothing accumulates, and a condensation shows up as a
|
|
302
|
+
* smaller total on the very next call.
|
|
303
|
+
* 3. Per-message estimates are then shares of that total, in proportion to each
|
|
304
|
+
* message's CURRENT size. Proportional-to-baseline is what makes a stub cost
|
|
305
|
+
* what a stub costs: the old scheme pinned a segment to its historical measured
|
|
306
|
+
* cost, so shrinking a message inside it merely moved tokens to its neighbours.
|
|
307
|
+
*
|
|
308
|
+
* The accuracy this buys is worth stating, because the old code's docblocks argued
|
|
309
|
+
* the opposite trade-off. Replayed call-by-call over the session above, the total's
|
|
310
|
+
* median absolute error against the prompt actually sent next falls from 76% to
|
|
311
|
+
* 0.3%. It is least accurate in two situations, both bounded by one call's change
|
|
312
|
+
* rather than accumulating: it over-states while a condensation is pending (the
|
|
313
|
+
* last measurement still describes the fat prompt), and it under-states a call
|
|
314
|
+
* that loads a large block of context in one step (nothing in stored history
|
|
315
|
+
* predicts it). The second is what a headroom reserve is for, not an estimator.
|
|
316
|
+
*
|
|
317
|
+
* `scale` is deliberately not used to compute the total — only to distribute it.
|
|
318
|
+
* It is noisy exactly where stored history is a poor proxy for the wire, and
|
|
319
|
+
* keeping the total anchored on a measurement rather than on `scale * baseline`
|
|
320
|
+
* is what stops that noise reaching the gate.
|
|
321
|
+
*/
|
|
322
|
+
function calibrate(history, preambleTokens) {
|
|
323
|
+
const baselines = history.map(baselineTokens);
|
|
324
|
+
let baselineTotal = 0;
|
|
325
|
+
for (const b of baselines)
|
|
326
|
+
baselineTotal += b;
|
|
327
|
+
const compactedAt = compactedAtOf(history);
|
|
328
|
+
const first = firstAnchor(history, compactedAt);
|
|
329
|
+
const last = lastAnchor(history, compactedAt);
|
|
330
|
+
// Nothing has ever reported usage, or a compaction invalidated every
|
|
331
|
+
// measurement. The char estimate is all there is for the MESSAGES — but a
|
|
332
|
+
// measured preamble is still authoritative, because it describes what was just
|
|
333
|
+
// built rather than being derived from an anchor. Only the derivation needs an
|
|
334
|
+
// anchor; a handed-in measurement does not, and discarding it here reported a
|
|
335
|
+
// preamble of zero on the two paths that reach this branch: a conversation
|
|
336
|
+
// before its first response, and the moment after a compaction (every retained
|
|
337
|
+
// message predates `compactedAt`, so no anchor survives). The second is the one
|
|
338
|
+
// that bit — `estimateRequestTokens` is the mid-turn overflow guard, and on a
|
|
339
|
+
// large tool surface it saw ~15 tokens instead of ~30,000.
|
|
340
|
+
//
|
|
341
|
+
// The preamble is added to `total` as well as reported as `overhead`, and both
|
|
342
|
+
// halves matter. In the anchored path below, `overhead` is carved OUT of a
|
|
343
|
+
// wire-measured total that already contains it; here there is no such total, so
|
|
344
|
+
// it has to be added or `messages` absorbs the difference. Reporting it in only
|
|
345
|
+
// one of the two places is worse than not reporting it at all: `projectCompaction`
|
|
346
|
+
// prices the preamble into `tokensAfter` and reads `total` for `tokensBefore`, so
|
|
347
|
+
// an overhead missing from the total makes every projection look like a net LOSS,
|
|
348
|
+
// `reclaimed` floors at zero, and `planCompaction` rejects the plan
|
|
349
|
+
// (`reclaimed >= minReclaimTokens`) — telling the user compaction cannot help on
|
|
350
|
+
// a transcript where it would reclaim plenty. Measured on the fixture in
|
|
351
|
+
// `context-tokens.test.ts`: reclaimed 8,814 → 0 with the total left alone.
|
|
352
|
+
//
|
|
353
|
+
// Nothing is invented: with no measurement passed this is exactly the old
|
|
354
|
+
// zero-overhead result, and `reclaimed` is unchanged either way, since the
|
|
355
|
+
// preamble then appears on both sides of `tokensBefore - tokensAfter`.
|
|
356
|
+
if (first < 0 || last < 0) {
|
|
357
|
+
const measured = Math.max(0, preambleTokens !== null && preambleTokens !== void 0 ? preambleTokens : 0);
|
|
358
|
+
return {
|
|
359
|
+
overhead: measured,
|
|
360
|
+
total: baselineTotal + measured,
|
|
361
|
+
estimates: baselines,
|
|
362
|
+
scale: 1,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
// The last measurement covers `history[0 … last-1]`; the messages from `last`
|
|
366
|
+
// onwards rode no measured prompt yet, so they are added at their char estimate.
|
|
367
|
+
let sinceLast = 0;
|
|
368
|
+
for (let i = last; i < baselines.length; i += 1)
|
|
369
|
+
sinceLast += baselines[i];
|
|
370
|
+
const total = history[last].inputTokens + sinceLast;
|
|
371
|
+
let beforeFirst = 0;
|
|
372
|
+
for (let i = 0; i < first; i += 1)
|
|
373
|
+
beforeFirst += baselines[i];
|
|
374
|
+
// A MEASURED preamble wins outright (GENC-1582). Only the total bounds it: the
|
|
375
|
+
// preamble cannot exceed a prompt that contains it.
|
|
376
|
+
//
|
|
377
|
+
// It is deliberately NOT capped against `baselineTotal`, which is what the
|
|
378
|
+
// previous cap did and why it had to go. `baselineTotal` counts stored
|
|
379
|
+
// CHARACTERS — full-fat, every tool payload intact, because `condenseWhen` works
|
|
380
|
+
// on a copy — while `total` is anchored on a wire-measured TOKEN count of an
|
|
381
|
+
// already-condensed prompt. Mid-run the former dwarfs the latter, so
|
|
382
|
+
// `total - baselineTotal` went negative and clamped the preamble to zero on
|
|
383
|
+
// exactly the condensed sessions this module exists to serve. Measured: 0 against
|
|
384
|
+
// full-fat stored history, 5,000 against the same session's post-condensation
|
|
385
|
+
// export — which is why the export-based check missed it. Comparing stored
|
|
386
|
+
// characters with wire tokens is the category error the whole file is about.
|
|
387
|
+
//
|
|
388
|
+
// Without a measurement we still fall back to the residual derivation, which is
|
|
389
|
+
// accurate when taken and goes stale on handover — see
|
|
390
|
+
// {@link estimatePreambleTokens} for why the fix was to stop deriving it.
|
|
391
|
+
const derived = Math.max(0, history[first].inputTokens - beforeFirst);
|
|
392
|
+
// A MEASURED preamble is trusted (GENC-1582) — but only up to the point where it
|
|
393
|
+
// starts contradicting a measurement, which is where an earlier version of this
|
|
394
|
+
// got it wrong.
|
|
395
|
+
//
|
|
396
|
+
// `total` is anchored on a WIRE measurement, and that measurement contains the
|
|
397
|
+
// preamble AND the messages that rode with it. So a preamble at or above `total`,
|
|
398
|
+
// on a history that HAS messages, is arithmetically impossible: it is proof the
|
|
399
|
+
// ESTIMATE is too high, not a fact about the conversation. `estimatePreambleTokens`
|
|
400
|
+
// converts characters to tokens with reasoned divisors rather than a tokenizer, and
|
|
401
|
+
// measured live on a large tool surface it reads about 12% high — 34,325 against a
|
|
402
|
+
// whole prompt the provider measured at 30,608.
|
|
403
|
+
//
|
|
404
|
+
// Letting it win there zeroed `messages`, which zeroes every share, which nulls the
|
|
405
|
+
// compaction plan — so the gate tells the user compaction cannot help on a transcript
|
|
406
|
+
// where it would in fact reclaim plenty. That is reachable on ANY window: it needs
|
|
407
|
+
// only a condensation to shrink the prompt below the preamble, which is routine after
|
|
408
|
+
// a long tool loop.
|
|
409
|
+
//
|
|
410
|
+
// The comment this replaces claimed the zero-shares state "is precisely the `'model'`
|
|
411
|
+
// block cause". It is not, and the two are not even related: `contextBlockCause` keys
|
|
412
|
+
// off `contextBudget.blockAt === 0`, i.e. the reserve against the WINDOW, and never
|
|
413
|
+
// consults this figure. Conflating "preamble exceeds the window" (rare, and genuinely
|
|
414
|
+
// means the model cannot host the agent) with "preamble exceeds the current transcript"
|
|
415
|
+
// (routine) is what put a dead end on the common path.
|
|
416
|
+
//
|
|
417
|
+
// So when the estimate is contradicted, fall back to the residual derivation, which is
|
|
418
|
+
// itself computed from a measured prompt. Zero shares remain possible only when there
|
|
419
|
+
// is genuinely nothing to price.
|
|
420
|
+
const claimed = preambleTokens !== null && preambleTokens !== void 0 ? preambleTokens : derived;
|
|
421
|
+
const contradicted = baselineTotal > 0 && claimed >= total;
|
|
422
|
+
const overhead = Math.min(contradicted ? derived : claimed, total);
|
|
423
|
+
const messages = Math.max(0, total - overhead);
|
|
424
|
+
if (baselineTotal <= 0) {
|
|
425
|
+
return { overhead, total, estimates: baselines, scale: 1 };
|
|
426
|
+
}
|
|
427
|
+
const scale = messages / baselineTotal;
|
|
428
|
+
const estimates = baselines.map((b) => Math.round(b * scale));
|
|
429
|
+
// Land the rounding residual on the heaviest message so the shares sum to
|
|
430
|
+
// EXACTLY `messages`. Heaviest rather than last: its share is at least the mean,
|
|
431
|
+
// which is comfortably more than the residual can ever be (half a token per
|
|
432
|
+
// message), so the correction cannot drive an estimate negative. Without this
|
|
433
|
+
// the per-message rounding drifts and every projection inherits the drift.
|
|
434
|
+
let assigned = 0;
|
|
435
|
+
let heaviest = -1;
|
|
436
|
+
for (let i = 0; i < estimates.length; i += 1) {
|
|
437
|
+
assigned += estimates[i];
|
|
438
|
+
if (baselines[i] > 0 && (heaviest < 0 || baselines[i] > baselines[heaviest]))
|
|
439
|
+
heaviest = i;
|
|
440
|
+
}
|
|
441
|
+
if (heaviest >= 0)
|
|
442
|
+
estimates[heaviest] += messages - assigned;
|
|
443
|
+
return { overhead, total, estimates, scale };
|
|
444
|
+
}
|
|
177
445
|
/**
|
|
178
446
|
* Whether anything about this transcript's size is knowable.
|
|
179
447
|
*
|
|
@@ -199,100 +467,55 @@ export function isContextMeasurable(history) {
|
|
|
199
467
|
return history.some((m) => m.inputTokens != null || m.role === 'compacted-summary');
|
|
200
468
|
}
|
|
201
469
|
/**
|
|
202
|
-
* Per-message token estimates
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* needed, since compaction only ever cuts on boundaries.
|
|
213
|
-
*
|
|
214
|
-
* A non-positive delta means history SHRANK between the two calls (a compaction
|
|
215
|
-
* or a `condenseWhen` collapse landed in between); there is nothing to
|
|
216
|
-
* distribute, so those messages keep their baseline.
|
|
217
|
-
*
|
|
218
|
-
* The region before the first anchor keeps its baseline too: that anchor's
|
|
219
|
-
* measurement also contains the system prompt and tool definitions, which are
|
|
220
|
-
* not attributable to any message — see {@link estimateSystemOverhead}.
|
|
470
|
+
* Per-message token estimates — each message's share of {@link calibrate}'s
|
|
471
|
+
* total, in proportion to how big that message is NOW.
|
|
472
|
+
*
|
|
473
|
+
* Used to choose and evaluate a compaction cut, so what matters is that the
|
|
474
|
+
* shares are in the same unit as the total and that they track the current size
|
|
475
|
+
* of each message. A payload `condenseWhen` has stubbed costs what the stub
|
|
476
|
+
* costs; under the segment-chained scheme this replaced, it kept the cost it had
|
|
477
|
+
* when it was full and the difference was merely redistributed to its neighbours.
|
|
478
|
+
*
|
|
479
|
+
* Sums to exactly `estimateContextTokens(history) - estimateSystemOverhead(history)`.
|
|
221
480
|
*/
|
|
222
|
-
export function estimateMessageTokens(history) {
|
|
223
|
-
|
|
224
|
-
const compactedAt = compactedAtOf(history);
|
|
225
|
-
const anchors = [];
|
|
226
|
-
history.forEach((m, i) => {
|
|
227
|
-
if (usableAnchor(m, compactedAt))
|
|
228
|
-
anchors.push(i);
|
|
229
|
-
});
|
|
230
|
-
for (let a = 0; a < anchors.length - 1; a += 1) {
|
|
231
|
-
const from = anchors[a];
|
|
232
|
-
const to = anchors[a + 1];
|
|
233
|
-
const measured = history[to].inputTokens - history[from].inputTokens;
|
|
234
|
-
if (measured <= 0)
|
|
235
|
-
continue;
|
|
236
|
-
let baseline = 0;
|
|
237
|
-
for (let i = from; i < to; i += 1)
|
|
238
|
-
baseline += estimates[i];
|
|
239
|
-
if (baseline <= 0) {
|
|
240
|
-
// Nothing to scale against (an all-display-only segment): put the measured
|
|
241
|
-
// cost on the first message rather than losing it.
|
|
242
|
-
estimates[from] = measured;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
// Rescale, then put the rounding residual on the last message of the segment
|
|
246
|
-
// so the segment sums to EXACTLY what was measured. Without it the per-message
|
|
247
|
-
// rounding drifts, and every projection inherits the drift of every segment
|
|
248
|
-
// before it.
|
|
249
|
-
const scale = measured / baseline;
|
|
250
|
-
let assigned = 0;
|
|
251
|
-
for (let i = from; i < to - 1; i += 1) {
|
|
252
|
-
estimates[i] = Math.round(estimates[i] * scale);
|
|
253
|
-
assigned += estimates[i];
|
|
254
|
-
}
|
|
255
|
-
estimates[to - 1] = measured - assigned;
|
|
256
|
-
}
|
|
257
|
-
return estimates;
|
|
481
|
+
export function estimateMessageTokens(history, preambleTokens) {
|
|
482
|
+
return calibrate(history, preambleTokens).estimates;
|
|
258
483
|
}
|
|
259
484
|
/**
|
|
260
485
|
* Context consumed by everything that is NOT a message — the system prompt and
|
|
261
486
|
* the tool definitions.
|
|
262
487
|
*
|
|
263
488
|
* Derived rather than guessed: the first measured prompt covers the messages
|
|
264
|
-
* before it plus
|
|
265
|
-
*
|
|
266
|
-
*
|
|
489
|
+
* before it plus the preamble, so subtracting our estimate of those messages
|
|
490
|
+
* leaves the preamble. Floored at zero, since an over-estimate of the early
|
|
491
|
+
* messages would otherwise produce a negative overhead, and capped at what the
|
|
492
|
+
* current total has left after paying for each message's characters — see
|
|
493
|
+
* {@link calibrate} for why that cap is load-bearing.
|
|
494
|
+
*
|
|
495
|
+
* It is NOT a constant, which the first cut of this assumed. Each agent brings its
|
|
496
|
+
* own system prompt and its own tool surface, so it steps up and down as agents
|
|
497
|
+
* hand over; what is true is that it does not shrink when the MESSAGES do.
|
|
267
498
|
*
|
|
268
499
|
* It matters for the gate because it is real context the user never sees in the
|
|
269
|
-
* transcript,
|
|
270
|
-
* tool surface it is far from negligible.
|
|
500
|
+
* transcript, and on an agent with a large tool surface it is far from negligible.
|
|
271
501
|
*/
|
|
272
|
-
export function estimateSystemOverhead(history) {
|
|
273
|
-
|
|
274
|
-
if (first < 0)
|
|
275
|
-
return 0;
|
|
276
|
-
const estimates = estimateMessageTokens(history);
|
|
277
|
-
let messages = 0;
|
|
278
|
-
for (let i = 0; i < first; i += 1)
|
|
279
|
-
messages += estimates[i];
|
|
280
|
-
return Math.max(0, history[first].inputTokens - messages);
|
|
502
|
+
export function estimateSystemOverhead(history, preambleTokens) {
|
|
503
|
+
return calibrate(history, preambleTokens).overhead;
|
|
281
504
|
}
|
|
282
505
|
/**
|
|
283
|
-
* Total estimated context for a transcript — the
|
|
506
|
+
* Total estimated context for a transcript — the figure the gauge shows and the
|
|
507
|
+
* composer gate is judged against.
|
|
508
|
+
*
|
|
509
|
+
* The most recent usable measurement plus the char estimate of everything
|
|
510
|
+
* appended since it, which is as close as anything gets to "how big is the prompt
|
|
511
|
+
* we are about to send". See {@link calibrate} for why it is anchored on that one
|
|
512
|
+
* measurement rather than accumulated across all of them.
|
|
284
513
|
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* that figure is that it stays defined for a hypothetical history (the tail a
|
|
288
|
-
* compaction would leave behind), which is what {@link projectCompaction} needs.
|
|
514
|
+
* Stays defined for a hypothetical history (the tail a compaction would leave
|
|
515
|
+
* behind), which is what {@link projectCompaction} needs.
|
|
289
516
|
*/
|
|
290
517
|
export function estimateContextTokens(history) {
|
|
291
|
-
|
|
292
|
-
let total = estimateSystemOverhead(history);
|
|
293
|
-
for (const t of estimates)
|
|
294
|
-
total += t;
|
|
295
|
-
return total;
|
|
518
|
+
return calibrate(history).total;
|
|
296
519
|
}
|
|
297
520
|
/**
|
|
298
521
|
* Estimated size of the request that will actually be SENT, given the stored
|
|
@@ -304,31 +527,34 @@ export function estimateContextTokens(history) {
|
|
|
304
527
|
* log. A guard that measured stored history would therefore refuse turns whose
|
|
305
528
|
* real request is a fraction of the size.
|
|
306
529
|
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
530
|
+
* So the request is estimated from its OWN characters, plus the preamble. Nothing
|
|
531
|
+
* is scaled: `requestHistory` IS the payload, so `baselineTokens` is already
|
|
532
|
+
* measuring the right bytes, and there is no transform left to correct for.
|
|
533
|
+
*
|
|
534
|
+
* **This previously applied a ratio, and a docblock here previously claimed it did
|
|
535
|
+
* not.** Both were wrong, and the second is the part worth recording. The original
|
|
536
|
+
* expression scaled the stored-history total by `requestChars / storedChars`,
|
|
537
|
+
* reasoning that `inputTokens` "measured the untransformed prompt". That premise is
|
|
538
|
+
* false — `inputTokens` measures the prompt that was SENT, transform already
|
|
539
|
+
* applied — and it is what made every figure in this module too large. The GENC-1582
|
|
540
|
+
* rewrite claimed to drop the ratio but only re-factored it: `scale` is
|
|
541
|
+
* `(total - overhead) / storedBaseline`, so `overhead + requestBaseline * scale` is
|
|
542
|
+
* the identical expression rearranged. With the total now anchored on a measurement
|
|
543
|
+
* of an already-condensed prompt, that discounted the same shrinkage twice —
|
|
544
|
+
* measured at **5,034 against a last measured prompt of 120,000**, i.e. a guard that
|
|
545
|
+
* believed it had 995k spare. Under-estimating is the one direction this figure must
|
|
546
|
+
* not err in, since its only job is refusing an overflow.
|
|
312
547
|
*
|
|
313
|
-
*
|
|
314
|
-
* characters
|
|
315
|
-
*
|
|
316
|
-
* prompt and tool definitions are not what condensation touches.
|
|
548
|
+
* The preamble must be passed in for the same reason: `requestBaseline` counts
|
|
549
|
+
* message characters, and the system prompt and tool definitions are not messages,
|
|
550
|
+
* so without it the guard silently ignores the entire tool surface.
|
|
317
551
|
*/
|
|
318
|
-
export function estimateRequestTokens(storedHistory, requestHistory) {
|
|
319
|
-
const overhead =
|
|
320
|
-
const calibratedMessages = Math.max(0, estimateContextTokens(storedHistory) - overhead);
|
|
321
|
-
let storedBaseline = 0;
|
|
322
|
-
for (const m of storedHistory)
|
|
323
|
-
storedBaseline += baselineTokens(m);
|
|
552
|
+
export function estimateRequestTokens(storedHistory, requestHistory, preambleTokens) {
|
|
553
|
+
const { overhead } = calibrate(storedHistory, preambleTokens);
|
|
324
554
|
let requestBaseline = 0;
|
|
325
555
|
for (const m of requestHistory)
|
|
326
556
|
requestBaseline += baselineTokens(m);
|
|
327
|
-
|
|
328
|
-
// request's own baseline is the best available answer.
|
|
329
|
-
if (storedBaseline <= 0)
|
|
330
|
-
return overhead + requestBaseline;
|
|
331
|
-
return Math.round(overhead + calibratedMessages * (requestBaseline / storedBaseline));
|
|
557
|
+
return Math.round(overhead + requestBaseline);
|
|
332
558
|
}
|
|
333
559
|
/**
|
|
334
560
|
* Project the effect of compacting at `cut`, WITHOUT running the summarizer.
|
|
@@ -342,16 +568,15 @@ export function estimateRequestTokens(storedHistory, requestHistory) {
|
|
|
342
568
|
* The system preamble is carried across unchanged: compaction removes messages,
|
|
343
569
|
* never the system prompt or the tool definitions.
|
|
344
570
|
*/
|
|
345
|
-
export function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKEN_ESTIMATE) {
|
|
346
|
-
|
|
347
|
-
|
|
571
|
+
export function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKEN_ESTIMATE, preambleTokens) {
|
|
572
|
+
// One calibration for both figures, so `tokensBefore` is the same number the
|
|
573
|
+
// gauge and the gate read. Deriving it independently is how an affordance ends
|
|
574
|
+
// up promising a reclaim measured against a total nothing else agrees with.
|
|
575
|
+
const { estimates, overhead, total } = calibrate(history, preambleTokens);
|
|
348
576
|
let tail = 0;
|
|
349
577
|
for (let i = cut; i < estimates.length; i += 1)
|
|
350
578
|
tail += estimates[i];
|
|
351
|
-
|
|
352
|
-
for (const t of estimates)
|
|
353
|
-
all += t;
|
|
354
|
-
const tokensBefore = overhead + all;
|
|
579
|
+
const tokensBefore = total;
|
|
355
580
|
const tokensAfter = overhead + summaryTokens + tail;
|
|
356
581
|
return {
|
|
357
582
|
cut,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { __rest } from "tslib";
|
|
2
|
-
import { estimateMessageTokens, projectCompaction, reachesProvider, } from './context-tokens';
|
|
2
|
+
import { COMPACTION_SUMMARY_TOKEN_ESTIMATE, estimateMessageTokens, projectCompaction, reachesProvider, } from './context-tokens';
|
|
3
3
|
/**
|
|
4
4
|
* Masks the tool-specific payload of a single message — clears tool call args
|
|
5
5
|
* and replaces tool result content with a placeholder. Used when passing history
|
|
@@ -152,8 +152,8 @@ export function isSafeCompactionCut(history, i) {
|
|
|
152
152
|
* enormous, indivisible tool group, which no cut can shrink because a cut cannot
|
|
153
153
|
* split a message.
|
|
154
154
|
*/
|
|
155
|
-
export function findCompactionCut(history, tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET) {
|
|
156
|
-
const estimates = estimateMessageTokens(history);
|
|
155
|
+
export function findCompactionCut(history, tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET, preambleTokens) {
|
|
156
|
+
const estimates = estimateMessageTokens(history, preambleTokens);
|
|
157
157
|
let messagesTotal = 0;
|
|
158
158
|
for (const t of estimates)
|
|
159
159
|
messagesTotal += t;
|
|
@@ -191,10 +191,10 @@ export function findCompactionCut(history, tailTokenBudget = DEFAULT_TAIL_TOKEN_
|
|
|
191
191
|
*/
|
|
192
192
|
export function planCompaction(history, options = {}) {
|
|
193
193
|
const { tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET, minReclaimTokens = DEFAULT_MIN_RECLAIM_TOKENS, } = options;
|
|
194
|
-
const cut = findCompactionCut(history, tailTokenBudget);
|
|
194
|
+
const cut = findCompactionCut(history, tailTokenBudget, options.preambleTokens);
|
|
195
195
|
if (cut == null)
|
|
196
196
|
return null;
|
|
197
|
-
const projection = projectCompaction(history, cut);
|
|
197
|
+
const projection = projectCompaction(history, cut, COMPACTION_SUMMARY_TOKEN_ESTIMATE, options.preambleTokens);
|
|
198
198
|
return projection.reclaimed >= minReclaimTokens ? projection : null;
|
|
199
199
|
}
|
|
200
200
|
/** One-line rendering of a message for the summarizer transcript. */
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { COMPACTION_SUMMARY_TOKEN_ESTIMATE } from './context-tokens';
|
|
2
1
|
// Re-exported, not redeclared: this module and `planCompaction` must apply the
|
|
3
2
|
// SAME worth-it floor, or a host passing explicit options and a caller relying
|
|
4
3
|
// on the module default would silently disagree about whether a compaction is
|
|
@@ -41,7 +40,7 @@ export const CONTEXT_ABORT_MARGIN_TOKENS = 4096;
|
|
|
41
40
|
*/
|
|
42
41
|
export function resolveContextBudget(input) {
|
|
43
42
|
var _a, _b, _c;
|
|
44
|
-
const { chatConfig, contextLimit, agentReserveTokens,
|
|
43
|
+
const { chatConfig, contextLimit, agentReserveTokens, agentWarnAheadTokens } = input;
|
|
45
44
|
const cfg = chatConfig.context;
|
|
46
45
|
const reserveTokens = Math.max(0, (_a = agentReserveTokens !== null && agentReserveTokens !== void 0 ? agentReserveTokens : cfg === null || cfg === void 0 ? void 0 : cfg.reserveTokens) !== null && _a !== void 0 ? _a : DEFAULT_RESERVE_TOKENS);
|
|
47
46
|
const warnMultiplier = Math.max(1, (_b = cfg === null || cfg === void 0 ? void 0 : cfg.warnMultiplier) !== null && _b !== void 0 ? _b : DEFAULT_WARN_MULTIPLIER);
|
|
@@ -60,11 +59,42 @@ export function resolveContextBudget(input) {
|
|
|
60
59
|
}
|
|
61
60
|
const enabled = (cfg === null || cfg === void 0 ? void 0 : cfg.enabled) !== false;
|
|
62
61
|
const blockAt = Math.max(0, limit - reserveTokens);
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
62
|
+
// An agent-declared warn-ahead is an ABSOLUTE distance above the block, so the
|
|
63
|
+
// warning sits a fixed, meaningful step ahead of it — one more of that agent's
|
|
64
|
+
// turns — instead of a multiple of a reserve it has no relation to. Only
|
|
65
|
+
// honoured alongside that agent's own reserve; on its own it would be measuring
|
|
66
|
+
// a step ahead of somebody else's line.
|
|
67
|
+
const warnAt = agentReserveTokens != null && agentWarnAheadTokens != null
|
|
68
|
+
? Math.max(0, limit - (reserveTokens + agentWarnAheadTokens))
|
|
69
|
+
: Math.max(0, limit - warnMultiplier * reserveTokens);
|
|
70
|
+
// Compaction's own target, deliberately NOT derived from `warnAt` (GENC-1582).
|
|
71
|
+
//
|
|
72
|
+
// `limit - 2 * reserve` is "leave room for about two of this agent's turns" of
|
|
73
|
+
// FREE headroom, so the retained tail shrinks as an agent's appetite grows: a
|
|
74
|
+
// 400k-reserve agent on a 1M window keeps 200,000 and compacts hard, while an
|
|
75
|
+
// agent on the ambient 25k keeps 950,000 and `findCompactionCut` correctly
|
|
76
|
+
// declines to do anything. That is the right way round — the expensive agent is
|
|
77
|
+
// the one that cannot afford history sitting alongside its turns.
|
|
78
|
+
//
|
|
79
|
+
// **This was coupled to `warnAt`, and the coupling was a real behaviour bug.**
|
|
80
|
+
// GENC-1567 (08f6504c44) introduced both this budget and the token-based cut, and
|
|
81
|
+
// aimed the budget at the warning line — coherent while `warnAt` meant "a whole
|
|
82
|
+
// reserve of room left". The GENC-1582 warn-ahead redefined `warnAt` as "one
|
|
83
|
+
// turn's residue left", which is what the warning wants and the opposite of what
|
|
84
|
+
// compaction wants, and this line inherited the new meaning silently: `warnAt`
|
|
85
|
+
// 200,000 -> 560,000 took the budget 400,000 -> 553,800 and cut the reclaim from
|
|
86
|
+
// ~195,000 to ~40,000. Since the budget drives `findCompactionCut`, which drives
|
|
87
|
+
// `compact()`, that is how much less gets summarized — not a display figure.
|
|
88
|
+
// Two unrelated questions ("when do we tell the user?" and "how much do we throw
|
|
89
|
+
// away?") were sharing one number; they no longer are.
|
|
90
|
+
//
|
|
91
|
+
// The `reserveTokens` floor went with it. It is what made the budget 400,000 in
|
|
92
|
+
// the first place, and on a tight window it produced a tail wider than the window
|
|
93
|
+
// had room for — a compaction that did nothing. A window that cannot host the
|
|
94
|
+
// agent is now reported as the `'model'` block cause instead of being papered
|
|
95
|
+
// over here. `systemOverhead` is gone for the same reason it stopped being
|
|
96
|
+
// derivable: see `estimatePreambleTokens`.
|
|
97
|
+
const tailTokenBudget = Math.max(0, limit - 2 * reserveTokens);
|
|
68
98
|
return {
|
|
69
99
|
enabled,
|
|
70
100
|
limit,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genesislcap/ai-assistant",
|
|
3
3
|
"description": "Genesis AI Assistant micro-frontend",
|
|
4
|
-
"version": "15.23.0",
|
|
4
|
+
"version": "15.23.1-FUI-0.4",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/ai-assistant.d.ts",
|
|
@@ -74,26 +74,26 @@
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@genesislcap/foundation-testing": "15.23.0",
|
|
78
|
-
"@genesislcap/genx": "15.23.0",
|
|
79
|
-
"@genesislcap/rollup-builder": "15.23.0",
|
|
80
|
-
"@genesislcap/ts-builder": "15.23.0",
|
|
81
|
-
"@genesislcap/uvu-playwright-builder": "15.23.0",
|
|
82
|
-
"@genesislcap/vite-builder": "15.23.0",
|
|
83
|
-
"@genesislcap/webpack-builder": "15.23.0",
|
|
77
|
+
"@genesislcap/foundation-testing": "15.23.1-FUI-0.4",
|
|
78
|
+
"@genesislcap/genx": "15.23.1-FUI-0.4",
|
|
79
|
+
"@genesislcap/rollup-builder": "15.23.1-FUI-0.4",
|
|
80
|
+
"@genesislcap/ts-builder": "15.23.1-FUI-0.4",
|
|
81
|
+
"@genesislcap/uvu-playwright-builder": "15.23.1-FUI-0.4",
|
|
82
|
+
"@genesislcap/vite-builder": "15.23.1-FUI-0.4",
|
|
83
|
+
"@genesislcap/webpack-builder": "15.23.1-FUI-0.4",
|
|
84
84
|
"@types/dompurify": "^3.0.5",
|
|
85
85
|
"@types/marked": "^5.0.2",
|
|
86
86
|
"esbuild": "0.25.12"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
|
-
"@genesislcap/foundation-ai": "15.23.0",
|
|
90
|
-
"@genesislcap/foundation-logger": "15.23.0",
|
|
91
|
-
"@genesislcap/foundation-notifications": "15.23.0",
|
|
92
|
-
"@genesislcap/foundation-redux": "15.23.0",
|
|
93
|
-
"@genesislcap/foundation-ui": "15.23.0",
|
|
94
|
-
"@genesislcap/foundation-utils": "15.23.0",
|
|
95
|
-
"@genesislcap/rapid-design-system": "15.23.0",
|
|
96
|
-
"@genesislcap/web-core": "15.23.0",
|
|
89
|
+
"@genesislcap/foundation-ai": "15.23.1-FUI-0.4",
|
|
90
|
+
"@genesislcap/foundation-logger": "15.23.1-FUI-0.4",
|
|
91
|
+
"@genesislcap/foundation-notifications": "15.23.1-FUI-0.4",
|
|
92
|
+
"@genesislcap/foundation-redux": "15.23.1-FUI-0.4",
|
|
93
|
+
"@genesislcap/foundation-ui": "15.23.1-FUI-0.4",
|
|
94
|
+
"@genesislcap/foundation-utils": "15.23.1-FUI-0.4",
|
|
95
|
+
"@genesislcap/rapid-design-system": "15.23.1-FUI-0.4",
|
|
96
|
+
"@genesislcap/web-core": "15.23.1-FUI-0.4",
|
|
97
97
|
"dompurify": "^3.3.1",
|
|
98
98
|
"marked": "^17.0.3"
|
|
99
99
|
},
|