u-foo 3.0.5 → 3.0.7
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/package.json +1 -1
- package/src/code/agent.js +42 -0
- package/src/code/context/planGraphService.js +140 -16
- package/src/code/context/planProjection.js +163 -62
- package/src/code/context/promptLayers.js +1 -0
- package/src/code/context/userInteraction.js +29 -4
- package/src/code/contextWindow.js +117 -0
- package/src/code/nativeRunner.js +116 -29
- package/src/code/runtime/agentWakeup.js +5 -3
- package/src/code/runtime/index.js +1 -0
- package/src/code/runtime/taskFocusContext.js +190 -0
- package/src/code/sessionStore.js +9 -0
- package/src/ui/format/index.js +8 -14
- package/src/ui/ink/ChatApp.js +20 -8
- package/src/ui/ink/UcodeApp.js +79 -6
- package/src/ui/ink/chatLogModel.js +4 -2
|
@@ -287,78 +287,94 @@ function buildPlanDag(planGraph = {}) {
|
|
|
287
287
|
};
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
function
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
return
|
|
290
|
+
function countDagProgress(dag = {}) {
|
|
291
|
+
const nodes = Array.isArray(dag.nodes) ? dag.nodes : [];
|
|
292
|
+
const total = nodes.length;
|
|
293
|
+
const done = nodes.filter((node) => node && node.kind === "done").length;
|
|
294
|
+
return { done, total };
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
297
|
+
function titleMaxForCols(cols = 80, reserved = 12) {
|
|
298
|
+
return Math.max(12, Math.min(48, Math.floor(Number(cols) || 80) - reserved));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function pickFocusActiveNodes(dag = {}) {
|
|
302
|
+
const nodes = Array.isArray(dag.nodes) ? dag.nodes : [];
|
|
303
|
+
const active = nodes.filter((node) => node && node.kind === "active");
|
|
304
|
+
if (active.length > 0) return active;
|
|
305
|
+
|
|
306
|
+
const waves = Array.isArray(dag.waves) ? dag.waves : [];
|
|
307
|
+
for (const wave of waves) {
|
|
308
|
+
const incomplete = (Array.isArray(wave) ? wave : []).filter((node) => (
|
|
309
|
+
node
|
|
310
|
+
&& node.kind !== "done"
|
|
311
|
+
&& node.kind !== "cancelled"
|
|
312
|
+
));
|
|
313
|
+
if (incomplete.length === 0) continue;
|
|
314
|
+
const ready = incomplete.filter((node) => String(node.status || "").toLowerCase() === "ready");
|
|
315
|
+
return ready.length > 0 ? ready : incomplete.slice(0, 1);
|
|
316
|
+
}
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function pickUpcomingNodes(dag = {}, activeIds = new Set(), limit = 2) {
|
|
321
|
+
const upcoming = [];
|
|
322
|
+
const waves = Array.isArray(dag.waves) ? dag.waves : [];
|
|
323
|
+
for (const wave of waves) {
|
|
324
|
+
for (const node of (Array.isArray(wave) ? wave : [])) {
|
|
325
|
+
if (!node || activeIds.has(node.id)) continue;
|
|
326
|
+
if (node.kind === "done" || node.kind === "cancelled") continue;
|
|
327
|
+
upcoming.push(node);
|
|
328
|
+
if (upcoming.length >= limit) return upcoming;
|
|
311
329
|
}
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
return
|
|
330
|
+
if (upcoming.length >= limit) break;
|
|
331
|
+
}
|
|
332
|
+
return upcoming;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function clipRoadmapLines(lines = [], maxRows = 10) {
|
|
336
|
+
const list = Array.isArray(lines) ? lines : [];
|
|
337
|
+
const limit = Number.isFinite(maxRows) && maxRows > 0 ? Math.floor(maxRows) : 10;
|
|
338
|
+
if (list.length <= limit) return list.slice();
|
|
339
|
+
const clipped = list.slice(0, Math.max(1, limit - 1));
|
|
340
|
+
clipped.push(`… +${list.length - clipped.length} more`);
|
|
341
|
+
return clipped;
|
|
315
342
|
}
|
|
316
343
|
|
|
317
344
|
/**
|
|
318
|
-
*
|
|
345
|
+
* Default auto band: progress + current task(s) + next titles.
|
|
346
|
+
* No ASCII tree, no 4a/4b labels.
|
|
319
347
|
*/
|
|
320
|
-
function
|
|
348
|
+
function buildFocusRoadmap(planGraph = {}, {
|
|
321
349
|
cols = 80,
|
|
322
350
|
taskRunLine = "",
|
|
323
|
-
maxRows =
|
|
351
|
+
maxRows = 4,
|
|
324
352
|
} = {}) {
|
|
325
353
|
const dag = buildPlanDag(planGraph);
|
|
326
354
|
if (dag.nodes.length === 0) {
|
|
327
355
|
return { markdown: "", lines: [], dag };
|
|
328
356
|
}
|
|
329
357
|
|
|
330
|
-
const titleMax =
|
|
331
|
-
const
|
|
332
|
-
const lines = [
|
|
358
|
+
const titleMax = titleMaxForCols(cols, 8);
|
|
359
|
+
const { done, total } = countDagProgress(dag);
|
|
360
|
+
const lines = [`**Plan** · ${done}/${total}`];
|
|
333
361
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
});
|
|
339
|
-
} else {
|
|
340
|
-
dag.waves.forEach((wave, waveIndex) => {
|
|
341
|
-
if (wave.length === 1) {
|
|
342
|
-
const node = wave[0];
|
|
343
|
-
lines.push(`${waveStepLabel(waveIndex, 0, 1)} ${node.mark} ${truncate(node.title, titleMax)}`);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
for (const line of formatParallelWaveLines(wave, waveIndex, Math.max(8, titleMax - 4))) {
|
|
347
|
-
lines.push(line);
|
|
348
|
-
}
|
|
349
|
-
});
|
|
362
|
+
const active = pickFocusActiveNodes(dag);
|
|
363
|
+
const activeIds = new Set(active.map((node) => node.id));
|
|
364
|
+
for (const node of active) {
|
|
365
|
+
lines.push(`${node.mark} ${truncate(node.title, titleMax)}`);
|
|
350
366
|
}
|
|
351
367
|
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
let clipped = lines.slice(0, Math.max(1, limit));
|
|
357
|
-
if (lines.length > clipped.length) {
|
|
358
|
-
clipped = clipped.slice(0, Math.max(1, limit - 1));
|
|
359
|
-
clipped.push(`… +${lines.length - clipped.length} more`);
|
|
368
|
+
const upcoming = pickUpcomingNodes(dag, activeIds, 2);
|
|
369
|
+
if (upcoming.length > 0) {
|
|
370
|
+
const titles = upcoming.map((node) => truncate(node.title, Math.max(8, Math.floor(titleMax / upcoming.length))));
|
|
371
|
+
lines.push(`接下来 · ${titles.join(" · ")}`);
|
|
360
372
|
}
|
|
361
373
|
|
|
374
|
+
const extra = String(taskRunLine || "").trim();
|
|
375
|
+
if (extra) lines.push(truncate(extra, Math.max(24, titleMax + 8)));
|
|
376
|
+
|
|
377
|
+
const clipped = clipRoadmapLines(lines, maxRows);
|
|
362
378
|
return {
|
|
363
379
|
markdown: clipped.join("\n"),
|
|
364
380
|
lines: clipped,
|
|
@@ -366,6 +382,89 @@ function buildRoadmapMarkdown(planGraph = {}, {
|
|
|
366
382
|
};
|
|
367
383
|
}
|
|
368
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Expanded (/plan focus): flat numbered list; parallel waves share a step number.
|
|
387
|
+
*/
|
|
388
|
+
function buildExpandedRoadmap(planGraph = {}, {
|
|
389
|
+
cols = 80,
|
|
390
|
+
taskRunLine = "",
|
|
391
|
+
maxRows = 16,
|
|
392
|
+
} = {}) {
|
|
393
|
+
const dag = buildPlanDag(planGraph);
|
|
394
|
+
if (dag.nodes.length === 0) {
|
|
395
|
+
return { markdown: "", lines: [], dag };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const titleMax = titleMaxForCols(cols, 14);
|
|
399
|
+
const { done, total } = countDagProgress(dag);
|
|
400
|
+
const objective = truncate(String(planGraph.objective || "").trim(), Math.max(12, titleMax - 8));
|
|
401
|
+
const header = objective
|
|
402
|
+
? `**Plan** · ${done}/${total} · ${objective}`
|
|
403
|
+
: `**Plan** · ${done}/${total}`;
|
|
404
|
+
const body = [];
|
|
405
|
+
|
|
406
|
+
dag.waves.forEach((wave, waveIndex) => {
|
|
407
|
+
const step = waveIndex + 1;
|
|
408
|
+
for (const node of wave) {
|
|
409
|
+
body.push(`${node.mark} ${step} ${truncate(node.title, titleMax)}`);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const extra = String(taskRunLine || "").trim();
|
|
414
|
+
if (extra) body.push(truncate(extra, Math.max(24, titleMax + 8)));
|
|
415
|
+
|
|
416
|
+
const budget = Math.max(1, (Number.isFinite(maxRows) ? Math.floor(maxRows) : 16) - 1);
|
|
417
|
+
let clippedBody = body;
|
|
418
|
+
if (body.length > budget) {
|
|
419
|
+
let windowStart = 0;
|
|
420
|
+
while (
|
|
421
|
+
windowStart < body.length
|
|
422
|
+
&& (body[windowStart].startsWith("✓") || body[windowStart].startsWith("⊘"))
|
|
423
|
+
) {
|
|
424
|
+
windowStart += 1;
|
|
425
|
+
}
|
|
426
|
+
// Keep one completed row before the live window for context.
|
|
427
|
+
windowStart = Math.max(0, windowStart - 1);
|
|
428
|
+
const window = body.slice(windowStart);
|
|
429
|
+
if (window.length <= budget) {
|
|
430
|
+
clippedBody = windowStart > 0
|
|
431
|
+
? [`… +${windowStart} more`, ...window]
|
|
432
|
+
: window;
|
|
433
|
+
} else {
|
|
434
|
+
const kept = window.slice(0, Math.max(1, budget - 1));
|
|
435
|
+
const omittedAfter = body.length - (windowStart + kept.length);
|
|
436
|
+
clippedBody = windowStart > 0
|
|
437
|
+
? [`… +${windowStart} more`, ...kept.slice(0, Math.max(1, budget - 2)), `… +${omittedAfter} more`]
|
|
438
|
+
: [...kept, `… +${omittedAfter} more`];
|
|
439
|
+
// If double ellipsis blew the budget, fall back to simple clip.
|
|
440
|
+
if (clippedBody.length > budget) {
|
|
441
|
+
clippedBody = clipRoadmapLines(body, budget);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const lines = [header, ...clippedBody];
|
|
447
|
+
return {
|
|
448
|
+
markdown: lines.join("\n"),
|
|
449
|
+
lines,
|
|
450
|
+
dag,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Build roadmap markdown from planGraph JSON.
|
|
456
|
+
* variant=focus (default auto band) or expanded (/plan focus).
|
|
457
|
+
*/
|
|
458
|
+
function buildRoadmapMarkdown(planGraph = {}, options = {}) {
|
|
459
|
+
const variant = String(options.variant || "focus").trim().toLowerCase() === "expanded"
|
|
460
|
+
? "expanded"
|
|
461
|
+
: "focus";
|
|
462
|
+
if (variant === "expanded") {
|
|
463
|
+
return buildExpandedRoadmap(planGraph, options);
|
|
464
|
+
}
|
|
465
|
+
return buildFocusRoadmap(planGraph, options);
|
|
466
|
+
}
|
|
467
|
+
|
|
369
468
|
function buildDebugLines(executionState = null, planGraph = {}) {
|
|
370
469
|
const lines = [];
|
|
371
470
|
const pg = planGraph && typeof planGraph === "object" ? planGraph : {};
|
|
@@ -491,27 +590,27 @@ function buildPlanUiProjection(executionState = null, options = {}) {
|
|
|
491
590
|
)];
|
|
492
591
|
roadmapMarkdown = "";
|
|
493
592
|
} else {
|
|
494
|
-
// auto
|
|
593
|
+
// auto → progress-focus; expanded → flat numbered list (no ASCII tree)
|
|
594
|
+
const variant = bandMode === "expanded" ? "expanded" : "focus";
|
|
495
595
|
const maxRows = Number.isFinite(options.maxBandRows)
|
|
496
596
|
? options.maxBandRows
|
|
497
|
-
: (
|
|
597
|
+
: (variant === "expanded" ? 16 : 4);
|
|
498
598
|
const roadmap = buildRoadmapMarkdown(pg, {
|
|
499
599
|
cols,
|
|
500
600
|
taskRunLine: taskRunSuffix,
|
|
501
601
|
maxRows,
|
|
602
|
+
variant,
|
|
502
603
|
});
|
|
503
604
|
planDag = roadmap.dag;
|
|
504
605
|
roadmapMarkdown = roadmap.markdown;
|
|
505
606
|
bandLines = roadmap.lines.slice();
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
bandLines
|
|
511
|
-
for (const row of tree) {
|
|
512
|
-
bandLines.push(truncate(formatTreeLine(row), Math.max(24, cols - 2)));
|
|
513
|
-
}
|
|
607
|
+
if (bandLines.length === 0 && tree.length > 0) {
|
|
608
|
+
const title = pg.objective ? `Plan · ${pg.objective}` : "Plan";
|
|
609
|
+
bandLines = [truncate(title, Math.max(24, cols - 2))];
|
|
610
|
+
for (const row of tree) {
|
|
611
|
+
bandLines.push(truncate(formatTreeLine(row), Math.max(24, cols - 2)));
|
|
514
612
|
}
|
|
613
|
+
roadmapMarkdown = "";
|
|
515
614
|
}
|
|
516
615
|
}
|
|
517
616
|
}
|
|
@@ -585,5 +684,7 @@ module.exports = {
|
|
|
585
684
|
statusToMark,
|
|
586
685
|
buildPlanDag,
|
|
587
686
|
buildRoadmapMarkdown,
|
|
687
|
+
buildFocusRoadmap,
|
|
688
|
+
buildExpandedRoadmap,
|
|
588
689
|
buildPlanUiProjection,
|
|
589
690
|
};
|
|
@@ -56,6 +56,7 @@ function buildImmutablePrefix() {
|
|
|
56
56
|
"- After an accepted plan_graph create or patch, Runtime automatically advances ready tool nodes. Never invent or request an execute_graph tool.",
|
|
57
57
|
"- Do not call plan_graph or task_run together with read, read_image, write, edit, bash, or artifact_read in the same assistant turn.",
|
|
58
58
|
"- When an active graph is waiting on a task, advance that node through plan_graph instead of bypassing it with direct workspace tools: use patch.expand_node for execution.kind=expand, control.complete_task (nodeId) for execution.kind=inline_llm, or control.start_task for execution.kind=task_loop.",
|
|
59
|
+
"- TaskLoop start returns childGraphId. While that TaskRun is waiting_model on child root, patch with graphId=<childGraphId> and expand_node nodeId=root (add tool children). Do not ask the user to /plan off.",
|
|
59
60
|
"- Do not end a turn with text only while the plan is still waiting on a task; expand, start, or complete that node. Runtime will auto-continue if you stop early, but prefer advancing in the same turn.",
|
|
60
61
|
"- control.complete_task with nodeId completes a waiting_llm inline_llm task for the current Graph owner. control.complete_task with taskRunId (or task_run complete) is reserved for the owning TaskLoop. Do not directly complete expand or aggregate tasks.",
|
|
61
62
|
"- While Plan Mode is ON, workspace mutations must be represented as plan_graph tool nodes or performed inside a running TaskRun/task_loop.",
|
|
@@ -387,15 +387,40 @@ function resolveUserInteraction(executionState = null, rawText = "") {
|
|
|
387
387
|
};
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
function
|
|
390
|
+
function wrapLabeledBlock(label = "", prompt = "", cols = 80) {
|
|
391
|
+
const title = String(label || "Question").trim() || "Question";
|
|
392
|
+
const text = String(prompt || "").replace(/\s+/g, " ").trim();
|
|
393
|
+
const width = Math.max(24, Math.min(120, Math.floor(Number(cols) || 80) - 2));
|
|
394
|
+
if (!text) return [`${title}:`];
|
|
395
|
+
|
|
396
|
+
const prefix = `${title}: `;
|
|
397
|
+
const firstWidth = Math.max(8, width - prefix.length);
|
|
398
|
+
const contPrefix = " ";
|
|
399
|
+
const contWidth = Math.max(8, width - contPrefix.length);
|
|
400
|
+
const lines = [];
|
|
401
|
+
|
|
402
|
+
let offset = 0;
|
|
403
|
+
const firstChunk = text.slice(0, firstWidth);
|
|
404
|
+
lines.push(`${prefix}${firstChunk}`);
|
|
405
|
+
offset = firstChunk.length;
|
|
406
|
+
while (offset < text.length) {
|
|
407
|
+
const chunk = text.slice(offset, offset + contWidth);
|
|
408
|
+
lines.push(`${contPrefix}${chunk}`);
|
|
409
|
+
offset += chunk.length;
|
|
410
|
+
}
|
|
411
|
+
return lines;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function formatInteractionPromptLines(pending = null, options = {}) {
|
|
391
415
|
if (!pending) return [];
|
|
416
|
+
const cols = Number(options.cols) > 0 ? Number(options.cols) : 80;
|
|
392
417
|
const lines = [];
|
|
393
418
|
const kind = pending.kind || "chat";
|
|
394
419
|
if (kind === "approval") {
|
|
395
|
-
lines.push(
|
|
420
|
+
lines.push(...wrapLabeledBlock("Approval", pending.prompt, cols));
|
|
396
421
|
lines.push(" [yes] Yes [no] No or type a free-text reply");
|
|
397
422
|
} else if (kind === "choice") {
|
|
398
|
-
lines.push(
|
|
423
|
+
lines.push(...wrapLabeledBlock("Choice", pending.prompt, cols));
|
|
399
424
|
for (const opt of pending.options || []) {
|
|
400
425
|
lines.push(` [${opt.key}] ${opt.label}`);
|
|
401
426
|
}
|
|
@@ -403,7 +428,7 @@ function formatInteractionPromptLines(pending = null) {
|
|
|
403
428
|
lines.push(" or type a free-text reply");
|
|
404
429
|
}
|
|
405
430
|
} else {
|
|
406
|
-
lines.push(
|
|
431
|
+
lines.push(...wrapLabeledBlock("Question", pending.prompt, cols));
|
|
407
432
|
lines.push(" (type your reply)");
|
|
408
433
|
}
|
|
409
434
|
return lines;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context-window helpers for the ucode TUI meter (used / limit in K).
|
|
5
|
+
*
|
|
6
|
+
* usedTokens comes from the latest model request's prompt occupancy.
|
|
7
|
+
* limitTokens is resolved from the model id (provider catalogs rarely
|
|
8
|
+
* expose a reliable context_window field).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function toTokenCount(value) {
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 0;
|
|
14
|
+
return Math.floor(parsed);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prompt-side tokens currently occupying the context window.
|
|
19
|
+
* Anthropic splits input / cache_read / cache_creation; OpenAI folds
|
|
20
|
+
* cache hits into prompt_tokens (cached_tokens is a subset).
|
|
21
|
+
*/
|
|
22
|
+
function contextTokensFromUsage(usage = null) {
|
|
23
|
+
if (!usage || typeof usage !== "object") return 0;
|
|
24
|
+
const input = toTokenCount(usage.input);
|
|
25
|
+
const cacheRead = toTokenCount(usage.cacheRead);
|
|
26
|
+
const cacheCreation = toTokenCount(usage.cacheCreation);
|
|
27
|
+
if (cacheCreation > 0) return input + cacheRead + cacheCreation;
|
|
28
|
+
// Anthropic exclusive split: input can be smaller than cache_read alone.
|
|
29
|
+
if (cacheRead > 0 && input < cacheRead) return input + cacheRead + cacheCreation;
|
|
30
|
+
// OpenAI-compatible: prompt_tokens already includes cached tokens.
|
|
31
|
+
return input;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveModelContextLimit(model = "", options = {}) {
|
|
35
|
+
const override = toTokenCount(options.limit || options.contextLimit);
|
|
36
|
+
if (override > 0) return override;
|
|
37
|
+
|
|
38
|
+
const id = String(model || "").trim().toLowerCase();
|
|
39
|
+
if (!id) return 200000;
|
|
40
|
+
|
|
41
|
+
if (/\b1m\b|1000000|million|1\.0m/.test(id)) return 1000000;
|
|
42
|
+
if (/256k/.test(id)) return 256000;
|
|
43
|
+
if (/128k/.test(id)) return 128000;
|
|
44
|
+
if (/64k/.test(id)) return 64000;
|
|
45
|
+
if (/32k/.test(id)) return 32000;
|
|
46
|
+
|
|
47
|
+
if (/claude|anthropic|opus|sonnet|haiku/.test(id)) return 200000;
|
|
48
|
+
if (/gemini|gemma/.test(id)) return 1000000;
|
|
49
|
+
if (/kimi|moonshot|k2\.|k2-|k3/.test(id)) return 256000;
|
|
50
|
+
if (/gpt-5|o3|o4|codex/.test(id)) return 200000;
|
|
51
|
+
if (/gpt-4\.1|gpt-4o|gpt-4-turbo|o1/.test(id)) return 128000;
|
|
52
|
+
if (/gpt-4|gpt-3\.5/.test(id)) return 128000;
|
|
53
|
+
|
|
54
|
+
return 200000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatTokensK(tokens = 0) {
|
|
58
|
+
const n = Math.max(0, Math.floor(Number(tokens) || 0));
|
|
59
|
+
if (n < 1000) return String(n);
|
|
60
|
+
const k = n / 1000;
|
|
61
|
+
if (k >= 100) return `${Math.round(k)}K`;
|
|
62
|
+
const tenths = Math.round(k * 10) / 10;
|
|
63
|
+
if (Number.isInteger(tenths)) return `${tenths}K`;
|
|
64
|
+
return `${tenths.toFixed(1)}K`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatContextMeter({ usedTokens = 0, limitTokens = 0 } = {}) {
|
|
68
|
+
const used = Math.max(0, Math.floor(Number(usedTokens) || 0));
|
|
69
|
+
const limit = Math.max(0, Math.floor(Number(limitTokens) || 0));
|
|
70
|
+
if (limit > 0) return `${formatTokensK(used)} / ${formatTokensK(limit)}`;
|
|
71
|
+
return formatTokensK(used);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildContextMeter({
|
|
75
|
+
usage = null,
|
|
76
|
+
usedTokens = null,
|
|
77
|
+
model = "",
|
|
78
|
+
limitTokens = null,
|
|
79
|
+
} = {}) {
|
|
80
|
+
const used = usedTokens != null
|
|
81
|
+
? toTokenCount(usedTokens)
|
|
82
|
+
: contextTokensFromUsage(usage);
|
|
83
|
+
const limit = resolveModelContextLimit(model, { limit: limitTokens });
|
|
84
|
+
return {
|
|
85
|
+
usedTokens: used,
|
|
86
|
+
limitTokens: limit,
|
|
87
|
+
model: String(model || "").trim(),
|
|
88
|
+
label: formatContextMeter({ usedTokens: used, limitTokens: limit }),
|
|
89
|
+
updatedAt: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeContextMeter(value = null, model = "") {
|
|
94
|
+
const source = value && typeof value === "object" ? value : {};
|
|
95
|
+
const used = toTokenCount(source.usedTokens);
|
|
96
|
+
const limit = resolveModelContextLimit(
|
|
97
|
+
String(source.model || model || "").trim(),
|
|
98
|
+
{ limit: source.limitTokens },
|
|
99
|
+
);
|
|
100
|
+
return {
|
|
101
|
+
usedTokens: used,
|
|
102
|
+
limitTokens: limit,
|
|
103
|
+
model: String(source.model || model || "").trim(),
|
|
104
|
+
label: formatContextMeter({ usedTokens: used, limitTokens: limit }),
|
|
105
|
+
updatedAt: String(source.updatedAt || "").trim(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
toTokenCount,
|
|
111
|
+
contextTokensFromUsage,
|
|
112
|
+
resolveModelContextLimit,
|
|
113
|
+
formatTokensK,
|
|
114
|
+
formatContextMeter,
|
|
115
|
+
buildContextMeter,
|
|
116
|
+
normalizeContextMeter,
|
|
117
|
+
};
|