@hncode/hncode 0.4.2 → 0.5.1
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/README.md +1 -15
- package/README_CN.md +1 -15
- package/bin/hncode +22 -9
- package/package.json +3 -2
- package/src/agent.js +149 -35
- package/src/bootstrap.js +56 -0
- package/src/btw-panel.js +224 -0
- package/src/cache.js +3 -2
- package/src/cell-buffer.js +173 -0
- package/src/ci.js +4 -1
- package/src/clipboard.js +202 -43
- package/src/colors.js +478 -242
- package/src/config.js +409 -40
- package/src/context-bar.js +237 -0
- package/src/doctor.js +518 -0
- package/src/experiments.js +122 -0
- package/src/file-history.js +112 -0
- package/src/file-tree.js +255 -0
- package/src/flush-tick.js +56 -0
- package/src/fork.js +44 -0
- package/src/fx.js +148 -0
- package/src/index.js +123 -11
- package/src/keybindings.js +229 -0
- package/src/llm.js +355 -34
- package/src/lsp.js +401 -0
- package/src/memory-profile.js +247 -0
- package/src/notebook.js +232 -0
- package/src/output-styles.js +183 -0
- package/src/patch.js +248 -0
- package/src/permissions.js +130 -1
- package/src/plan.js +3 -2
- package/src/plugin.js +611 -58
- package/src/prompt-presets.js +1 -1
- package/src/registry-browser.js +31 -13
- package/src/remote.js +20 -2
- package/src/rollout.js +151 -0
- package/src/row-cache.js +148 -0
- package/src/schedule.js +294 -0
- package/src/session-approvals.js +86 -0
- package/src/session-hub.js +1 -0
- package/src/session.js +178 -8
- package/src/settings-schema.js +301 -0
- package/src/shell-safety.js +473 -0
- package/src/side-thread.js +240 -0
- package/src/skills.js +27 -0
- package/src/stream-buffer.js +350 -0
- package/src/subagent-control.js +160 -0
- package/src/teleport.js +260 -0
- package/src/term-caps.js +411 -0
- package/src/term.js +283 -8
- package/src/tool-result.js +264 -236
- package/src/tools/agent-control.js +98 -0
- package/src/tools/agent-swarm.js +11 -2
- package/src/tools/agent.js +35 -1
- package/src/tools/diagnostics.js +175 -0
- package/src/tools/edit.js +25 -10
- package/src/tools/grep.js +62 -20
- package/src/tools/ignore.js +38 -14
- package/src/tools/index.js +10 -0
- package/src/tools/notebook.js +113 -0
- package/src/tools/read-media-file.js +10 -3
- package/src/tools/shell.js +34 -6
- package/src/tui.js +5637 -746
- package/src/web-public/app.js +7 -0
- package/src/web.js +3 -1
package/src/term.js
CHANGED
|
@@ -167,6 +167,113 @@ function isWideByDefault(c) {
|
|
|
167
167
|
|| (c >= 0x2B1B && c <= 0x2B1C) || c === 0x2B50 || c === 0x2B55;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// East Asian AMBIGUOUS width
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Unicode gives every character an East Asian Width class. `A` (Ambiguous) means
|
|
174
|
+
// ONE column on a Western terminal and TWO on a CJK one — the terminal decides, the
|
|
175
|
+
// character does not say. hncode's chrome leans on these heavily, which is why they
|
|
176
|
+
// had to be handled rather than avoided:
|
|
177
|
+
//
|
|
178
|
+
// box rules ─ │ ╭ ╮ ╰ ╯ ┌ ┐ └ ┘ ├ ┤ ┬ ┴ ┼
|
|
179
|
+
// separators · … — –
|
|
180
|
+
// markers ↑ ↓ ← → ❯ ● ○ ✓ ✗
|
|
181
|
+
// math ± × ÷ ≠ ≤ ≥ ∞
|
|
182
|
+
//
|
|
183
|
+
// Measuring them as 1 column on a CJK terminal made every row containing one N columns
|
|
184
|
+
// short, so the row's right edge landed N columns past the panel it belonged to —
|
|
185
|
+
// "the text pushes the wall right". The panel's own background could not hide it
|
|
186
|
+
// either: the overflow is REAL cells on screen, not a measurement disagreement inside
|
|
187
|
+
// one row.
|
|
188
|
+
//
|
|
189
|
+
// Default: follow the LOCALE. A CJK locale is the standard reason a terminal treats
|
|
190
|
+
// EAW=A as wide, and it is the only signal available before anything is drawn — the
|
|
191
|
+
// code page does not say (a UTF-8 Windows console still renders them wide), and no
|
|
192
|
+
// terminal query reports it.
|
|
193
|
+
let ambiguousWide = null; // lazily resolved so a test can set the env first
|
|
194
|
+
|
|
195
|
+
/** EAW=A ranges (Unicode 15), ascending — `inRanges` binary-searches this. */
|
|
196
|
+
const AMBIGUOUS = [
|
|
197
|
+
[0x00A1, 0x00A1], [0x00A4, 0x00A4], [0x00A7, 0x00A8], [0x00AA, 0x00AA],
|
|
198
|
+
[0x00AD, 0x00AE], [0x00B0, 0x00B4], [0x00B6, 0x00BA], [0x00BC, 0x00BF],
|
|
199
|
+
[0x00C6, 0x00C6], [0x00D0, 0x00D0], [0x00D7, 0x00D8], [0x00DE, 0x00E1],
|
|
200
|
+
[0x00E6, 0x00E6], [0x00E8, 0x00EA], [0x00EC, 0x00ED], [0x00F0, 0x00F0],
|
|
201
|
+
[0x00F2, 0x00F3], [0x00F7, 0x00FA], [0x00FC, 0x00FC], [0x00FE, 0x00FE],
|
|
202
|
+
[0x0101, 0x0101], [0x0111, 0x0111], [0x0113, 0x0113], [0x011B, 0x011B],
|
|
203
|
+
[0x0126, 0x0127], [0x012B, 0x012B], [0x0131, 0x0133], [0x0138, 0x0138],
|
|
204
|
+
[0x013F, 0x0142], [0x0144, 0x0144], [0x0148, 0x014B], [0x014D, 0x014D],
|
|
205
|
+
[0x0152, 0x0153], [0x0166, 0x0167], [0x016B, 0x016B], [0x01CE, 0x01CE],
|
|
206
|
+
[0x01D0, 0x01D0], [0x01D2, 0x01D2], [0x01D4, 0x01D4], [0x01D6, 0x01D6],
|
|
207
|
+
[0x01D8, 0x01D8], [0x01DA, 0x01DA], [0x01DC, 0x01DC], [0x0251, 0x0251],
|
|
208
|
+
[0x0261, 0x0261], [0x02C4, 0x02C4], [0x02C7, 0x02C7], [0x02C9, 0x02CB],
|
|
209
|
+
[0x02CD, 0x02CD], [0x02D0, 0x02D0], [0x02D8, 0x02DB], [0x02DD, 0x02DD],
|
|
210
|
+
[0x02DF, 0x02DF], [0x0300, 0x036F], [0x0391, 0x03A1], [0x03A3, 0x03A9],
|
|
211
|
+
[0x03B1, 0x03C1], [0x03C3, 0x03C9], [0x0401, 0x0401], [0x0410, 0x044F],
|
|
212
|
+
[0x0451, 0x0451], [0x2010, 0x2010], [0x2013, 0x2016], [0x2018, 0x2019],
|
|
213
|
+
[0x201C, 0x201D], [0x2020, 0x2022], [0x2024, 0x2027], [0x2030, 0x2030],
|
|
214
|
+
[0x2032, 0x2033], [0x2035, 0x2035], [0x203B, 0x203B], [0x203E, 0x203E],
|
|
215
|
+
[0x2074, 0x2074], [0x207F, 0x207F], [0x2081, 0x2084], [0x20AC, 0x20AC],
|
|
216
|
+
[0x2103, 0x2103], [0x2105, 0x2105], [0x2109, 0x2109], [0x2113, 0x2113],
|
|
217
|
+
[0x2116, 0x2116], [0x2121, 0x2122], [0x2126, 0x2126], [0x212B, 0x212B],
|
|
218
|
+
[0x2153, 0x2154], [0x215B, 0x215E], [0x2160, 0x216B], [0x2170, 0x2179],
|
|
219
|
+
[0x2189, 0x2189], [0x2190, 0x2199], [0x21B8, 0x21B9], [0x21D2, 0x21D2],
|
|
220
|
+
[0x21D4, 0x21D4], [0x21E7, 0x21E7], [0x2200, 0x2200], [0x2202, 0x2203],
|
|
221
|
+
[0x2207, 0x2208], [0x220B, 0x220B], [0x220F, 0x220F], [0x2211, 0x2211],
|
|
222
|
+
[0x2215, 0x2215], [0x221A, 0x221A], [0x221D, 0x2220], [0x2223, 0x2223],
|
|
223
|
+
[0x2225, 0x2225], [0x2227, 0x222C], [0x222E, 0x222E], [0x2234, 0x2237],
|
|
224
|
+
[0x223C, 0x223D], [0x2248, 0x2248], [0x224C, 0x224C], [0x2252, 0x2252],
|
|
225
|
+
[0x2260, 0x2261], [0x2264, 0x2267], [0x226A, 0x226B], [0x226E, 0x226F],
|
|
226
|
+
[0x2282, 0x2283], [0x2286, 0x2287], [0x2295, 0x2295], [0x2299, 0x2299],
|
|
227
|
+
[0x22A5, 0x22A5], [0x22BF, 0x22BF], [0x2312, 0x2312], [0x2460, 0x24E9],
|
|
228
|
+
[0x24EB, 0x254B], [0x2550, 0x2573], [0x2580, 0x258F], [0x2592, 0x2595],
|
|
229
|
+
[0x25A0, 0x25A1], [0x25A3, 0x25A9], [0x25B2, 0x25B3], [0x25B6, 0x25B7],
|
|
230
|
+
[0x25BC, 0x25BD], [0x25C0, 0x25C1], [0x25C6, 0x25C8], [0x25CB, 0x25CB],
|
|
231
|
+
[0x25CE, 0x25D1], [0x25E2, 0x25E5], [0x25EF, 0x25EF], [0x2605, 0x2606],
|
|
232
|
+
[0x2609, 0x2609], [0x260E, 0x260F], [0x261C, 0x261C], [0x261E, 0x261E],
|
|
233
|
+
[0x2640, 0x2640], [0x2642, 0x2642], [0x2660, 0x2661], [0x2663, 0x2665],
|
|
234
|
+
[0x2667, 0x266A], [0x266C, 0x266D], [0x266F, 0x266F], [0x269E, 0x269F],
|
|
235
|
+
[0x26C6, 0x26CD], [0x26CF, 0x26D3], [0x26D5, 0x26E1], [0x26E3, 0x26E3],
|
|
236
|
+
[0x26E8, 0x26E9], [0x26EB, 0x26F1], [0x26F4, 0x26F4], [0x26F6, 0x26F9],
|
|
237
|
+
[0x26FB, 0x26FC], [0x26FE, 0x26FF], [0x273D, 0x273D], [0x2761, 0x2761],
|
|
238
|
+
[0x2776, 0x277F], [0x2B56, 0x2B59], [0x3248, 0x324F], [0xE000, 0xF8FF],
|
|
239
|
+
[0xFE00, 0xFE0F], [0xFFFD, 0xFFFD], [0x1F100, 0x1F10A],
|
|
240
|
+
[0x1F110, 0x1F12D], [0x1F130, 0x1F169], [0x1F170, 0x1F19A],
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Should EAW=Ambiguous characters measure 2 columns?
|
|
245
|
+
*
|
|
246
|
+
* Resolved once, from (in order):
|
|
247
|
+
* 1. `HNCODE_AMBIGUOUS_WIDTH` — an explicit override, for a terminal whose setting
|
|
248
|
+
* does not match its locale;
|
|
249
|
+
* 2. the locale — `zh`/`ja`/`ko` mean a CJK terminal, which is the standard reason a
|
|
250
|
+
* terminal renders these wide.
|
|
251
|
+
*/
|
|
252
|
+
export function detectAmbiguousWide(env = process.env) {
|
|
253
|
+
const raw = env.HNCODE_AMBIGUOUS_WIDTH;
|
|
254
|
+
if (raw !== undefined && raw !== '') return /^(1|true|yes|on)$/i.test(raw);
|
|
255
|
+
let loc = '';
|
|
256
|
+
try { loc = (Intl.DateTimeFormat().resolvedOptions().locale) || ''; } catch { /* no Intl */ }
|
|
257
|
+
const tag = loc || env.LC_ALL || env.LC_CTYPE || env.LANG || '';
|
|
258
|
+
return /^(zh|ja|ko)\b/i.test(tag) || /^(zh|ja|ko)[-_]/i.test(tag);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function isAmbiguousWide() {
|
|
262
|
+
if (ambiguousWide === null) ambiguousWide = detectAmbiguousWide();
|
|
263
|
+
return ambiguousWide;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Turn the ambiguous-as-wide rule on or off. Clears the width cache, which is keyed by
|
|
268
|
+
* string alone and would otherwise hand back widths measured under the other rule.
|
|
269
|
+
*/
|
|
270
|
+
export function setAmbiguousWide(on) {
|
|
271
|
+
const next = !!on;
|
|
272
|
+
if (next === ambiguousWide) return;
|
|
273
|
+
ambiguousWide = next;
|
|
274
|
+
widthCache.clear();
|
|
275
|
+
}
|
|
276
|
+
|
|
170
277
|
const inRanges = (c, ranges) => {
|
|
171
278
|
let lo = 0, hi = ranges.length - 1;
|
|
172
279
|
while (lo <= hi) {
|
|
@@ -255,9 +362,46 @@ function clusterGlyphWidth(cluster) {
|
|
|
255
362
|
// tokens = ceil(ASCII chars / 4) + non-ASCII chars
|
|
256
363
|
// ASCII characters average ~4 per token; CJK and other non-ASCII are ~1 token
|
|
257
364
|
// each. This is far more accurate than a uniform chars/4 ratio.
|
|
365
|
+
// Token estimate. The character walk is O(len), and the context gauge re-prices
|
|
366
|
+
// whole message bodies (a Read result can be hundreds of KB) every turn.
|
|
367
|
+
//
|
|
368
|
+
// A value cache only helps COMPLETED messages: they are immutable, so the same
|
|
369
|
+
// string is measured again and again. It actively HURTS streaming text, where the
|
|
370
|
+
// string grows every frame — every call is a miss, so the cache pays hashing and an
|
|
371
|
+
// insert each time and (once full) an O(n) eviction sweep, measuring 1ms/call vs
|
|
372
|
+
// ~0.001ms for a plain walk (30k growing-text calls: 31s cached vs 30ms uncached).
|
|
373
|
+
//
|
|
374
|
+
// So the cache is limited to SHORT strings: completed metadata and role names hit
|
|
375
|
+
// it, while a long or growing body is just walked directly. At most 4 KB was chosen
|
|
376
|
+
// because a token estimate is only ever re-asked for values that are cheap to hold
|
|
377
|
+
// and genuinely stable; anything larger is better re-walked than hashed + stored.
|
|
378
|
+
const tokCache = new Map();
|
|
379
|
+
const TOK_CACHE_MAX = 20000;
|
|
380
|
+
const TOK_CACHE_MAX_LEN = 4096; // only memoise strings this short
|
|
381
|
+
|
|
258
382
|
export function estimateTokens(text) {
|
|
383
|
+
const s = String(text ?? '');
|
|
384
|
+
if (s.length <= TOK_CACHE_MAX_LEN) {
|
|
385
|
+
const hit = tokCache.get(s);
|
|
386
|
+
if (hit !== undefined) return hit;
|
|
387
|
+
let ascii = 0, nonAscii = 0;
|
|
388
|
+
for (const ch of s) {
|
|
389
|
+
if (ch.codePointAt(0) <= 127) ascii++;
|
|
390
|
+
else nonAscii++;
|
|
391
|
+
}
|
|
392
|
+
const out = Math.ceil(ascii / 4) + nonAscii;
|
|
393
|
+
if (tokCache.size >= TOK_CACHE_MAX) {
|
|
394
|
+
// Drop the oldest half in one go: cheaper than one-at-a-time and keeps the
|
|
395
|
+
// recently measured entries that a per-entry eviction would churn.
|
|
396
|
+
let i = 0;
|
|
397
|
+
for (const k of tokCache.keys()) { tokCache.delete(k); if (++i >= TOK_CACHE_MAX / 2) break; }
|
|
398
|
+
}
|
|
399
|
+
tokCache.set(s, out);
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
// Long body: walk it. No memoise, no map churn.
|
|
259
403
|
let ascii = 0, nonAscii = 0;
|
|
260
|
-
for (const ch of
|
|
404
|
+
for (const ch of s) {
|
|
261
405
|
if (ch.codePointAt(0) <= 127) ascii++;
|
|
262
406
|
else nonAscii++;
|
|
263
407
|
}
|
|
@@ -273,17 +417,148 @@ export function estimateTokens(text) {
|
|
|
273
417
|
// estimateRequestOverhead().
|
|
274
418
|
export function estimateMessagesTokens(messages, cfg) {
|
|
275
419
|
let total = 0;
|
|
276
|
-
for (const m of messages || [])
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
420
|
+
for (const m of messages || []) total += estimateMessageTokens(m, cfg);
|
|
421
|
+
return total;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Tokens for ONE message. Split out so callers that walk the history (e.g.
|
|
426
|
+
* planCompaction) can price each message ONCE instead of re-summing the whole
|
|
427
|
+
* array per message — that was O(n²) over the transcript.
|
|
428
|
+
*/
|
|
429
|
+
export function estimateMessageTokens(m, cfg) {
|
|
430
|
+
if (!m) return 0;
|
|
431
|
+
let total = estimateTokens(m.role || '');
|
|
432
|
+
const c = m.content;
|
|
433
|
+
total += typeof c === 'string' ? estimateTokens(c) : estimateTokens(JSON.stringify(c || ''));
|
|
434
|
+
if (m.toolCalls) for (const tc of m.toolCalls) {
|
|
435
|
+
total += estimateTokens(tc.name || '') + estimateTokens(JSON.stringify(tc.args || ''));
|
|
283
436
|
}
|
|
284
437
|
return total;
|
|
285
438
|
}
|
|
286
439
|
|
|
440
|
+
// ---- per-message token memo -----------------------------------------------
|
|
441
|
+
//
|
|
442
|
+
// Message objects -> { sig, tok }. WeakMap, so a message dropped from the history
|
|
443
|
+
// takes its estimate with it: no unbounded growth, and no manual eviction policy.
|
|
444
|
+
const msgTokMemo = new WeakMap();
|
|
445
|
+
//
|
|
446
|
+
// `estimateMessageTokens` above re-measures an immutable message every time it is
|
|
447
|
+
// asked, and the askers are frequent: the context gauge runs at every step, and
|
|
448
|
+
// compaction walks the whole history to find its cut point. A Read result can be
|
|
449
|
+
// hundreds of KB, so re-walking one costs milliseconds (measured: a 300 KB body is
|
|
450
|
+
// ~5.3 ms to price). Completed messages never change, so their estimate never has
|
|
451
|
+
// to be recomputed.
|
|
452
|
+
//
|
|
453
|
+
// The memo is a WeakMap keyed on the MESSAGE OBJECT, which gives two properties for
|
|
454
|
+
// free: it cannot leak (a dropped message takes its entry with it) and it cannot
|
|
455
|
+
// collide (two equal messages are still two objects).
|
|
456
|
+
//
|
|
457
|
+
// Correctness — the one place a stored message changes underneath us —
|
|
458
|
+
// --------------------------------------------------------------------
|
|
459
|
+
// `trimToolResults` rewrites `m.content` IN PLACE, replacing a large body with a
|
|
460
|
+
// short pointer. A memo that trusted object identity alone would keep serving the
|
|
461
|
+
// pre-trim count, and the gauge would claim the context was still full right after
|
|
462
|
+
// the very operation meant to shrink it — the trim would appear to do nothing and
|
|
463
|
+
// the next request would compact again for no reason.
|
|
464
|
+
//
|
|
465
|
+
// So every entry stores a cheap SIGNATURE of the content alongside the number, and
|
|
466
|
+
// a mismatch recomputes. `.length` on a string is O(1), which is what makes this
|
|
467
|
+
// affordable: streaming text (which grows every frame) misses on the signature and
|
|
468
|
+
// is re-walked, exactly as `estimateTokens`' own short-string cache already
|
|
469
|
+
// established. Content that is neither a string nor an array gets a weaker
|
|
470
|
+
// signature and is simply re-measured each time rather than risk a stale answer.
|
|
471
|
+
function msgSignature(m) {
|
|
472
|
+
const c = m.content;
|
|
473
|
+
let s;
|
|
474
|
+
if (typeof c === 'string') s = 's' + c.length;
|
|
475
|
+
else if (c == null) s = 'n';
|
|
476
|
+
else if (Array.isArray(c)) s = 'a' + c.length;
|
|
477
|
+
else s = ''; // unknown shape: never trust a memo
|
|
478
|
+
if (m.toolCalls) s += 't' + m.toolCalls.length;
|
|
479
|
+
return s;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Drop any memoised estimate for a message whose content was rewritten in place. */
|
|
483
|
+
export function forgetMessageTokens(m) {
|
|
484
|
+
if (m && typeof m === 'object') msgTokMemo.delete(m);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* `estimateMessageTokens`, memoised per message object.
|
|
489
|
+
*
|
|
490
|
+
* Safe to call on STREAMING messages too: the signature check misses on every
|
|
491
|
+
* frame, so a growing body is simply re-measured and never served stale.
|
|
492
|
+
*/
|
|
493
|
+
export function messageTokens(m, cfg) {
|
|
494
|
+
if (!m || typeof m !== 'object') return estimateMessageTokens(m, cfg);
|
|
495
|
+
const sig = msgSignature(m);
|
|
496
|
+
if (sig !== '') {
|
|
497
|
+
const hit = msgTokMemo.get(m);
|
|
498
|
+
if (hit !== undefined && hit.sig === sig) return hit.tok;
|
|
499
|
+
}
|
|
500
|
+
const tok = estimateMessageTokens(m, cfg);
|
|
501
|
+
if (sig !== '') msgTokMemo.set(m, { sig, tok });
|
|
502
|
+
return tok;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* A rolling total over a message list that grows by appending.
|
|
507
|
+
*
|
|
508
|
+
* This is the idea borrowed from jcode's `ActiveCharEstimate`
|
|
509
|
+
* (`crates/jcode-base/src/compaction.rs`): keep a running count of the messages
|
|
510
|
+
* already priced, so the common append-only case never rescans history. jcode also
|
|
511
|
+
* notes why the cached value and its staleness flag are bundled into one type
|
|
512
|
+
* rather than two independent fields — a path that updated one without the other
|
|
513
|
+
* silently corrupted token accounting. Here the two are a single number,
|
|
514
|
+
* `_counted`: it is meaningless on its own and is only ever read together with the
|
|
515
|
+
* length it was computed against.
|
|
516
|
+
*
|
|
517
|
+
* `sync()` is O(new messages) when the list was only appended to, and O(n) after a
|
|
518
|
+
* compaction (which REPLACES the array, so the old count describes messages that no
|
|
519
|
+
* longer exist). Correctness does not depend on detecting appends perfectly: the
|
|
520
|
+
* boundary identities are spot-checked and any doubt falls back to a full recount,
|
|
521
|
+
* which is still cheap because the per-message memo makes each step a WeakMap hit.
|
|
522
|
+
*/
|
|
523
|
+
export class TokenLedger {
|
|
524
|
+
constructor() {
|
|
525
|
+
this.tokens = 0;
|
|
526
|
+
this._counted = 0;
|
|
527
|
+
this._first = null;
|
|
528
|
+
this._last = null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Total tokens for `messages`, reusing the previous count where it is still valid. */
|
|
532
|
+
sync(messages, cfg) {
|
|
533
|
+
const msgs = Array.isArray(messages) ? messages : [];
|
|
534
|
+
const n = msgs.length;
|
|
535
|
+
const canExtend = this._counted > 0 && this._counted <= n
|
|
536
|
+
&& this._first === msgs[0]
|
|
537
|
+
&& this._last === msgs[this._counted - 1];
|
|
538
|
+
if (canExtend) {
|
|
539
|
+
for (let i = this._counted; i < n; i++) this.tokens += messageTokens(msgs[i], cfg);
|
|
540
|
+
} else {
|
|
541
|
+
// Full recount. Still O(n) WEAK-MAP LOOKUPS rather than O(n) string walks,
|
|
542
|
+
// which is the difference between milliseconds and microseconds.
|
|
543
|
+
let total = 0;
|
|
544
|
+
for (let i = 0; i < n; i++) total += messageTokens(msgs[i], cfg);
|
|
545
|
+
this.tokens = total;
|
|
546
|
+
}
|
|
547
|
+
this._counted = n;
|
|
548
|
+
this._first = n ? msgs[0] : null;
|
|
549
|
+
this._last = n ? msgs[n - 1] : null;
|
|
550
|
+
return this.tokens;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Forget everything — call after an in-place edit of an already-counted message. */
|
|
554
|
+
reset() {
|
|
555
|
+
this.tokens = 0;
|
|
556
|
+
this._counted = 0;
|
|
557
|
+
this._first = null;
|
|
558
|
+
this._last = null;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
287
562
|
// The overhead a request carries BEYOND its messages: the system prompt plus the
|
|
288
563
|
// tool definitions. Passed the assembled system text so the number matches what is
|
|
289
564
|
// actually sent.
|