u-foo 3.0.6 → 3.0.8
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/executionSegment.js +1 -0
- package/src/code/context/planGraphService.js +261 -55
- package/src/code/context/planProjection.js +163 -62
- package/src/code/context/promptLayers.js +1 -0
- package/src/code/context/userNudge.js +13 -9
- package/src/code/contextWindow.js +117 -0
- package/src/code/nativeRunner.js +116 -29
- 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 +87 -4
- 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
|
};
|
|
@@ -54,6 +54,7 @@ function buildImmutablePrefix() {
|
|
|
54
54
|
"- Turning Plan Mode off does not cancel an existing graph or running TaskRuns. Cancel with task_run (standalone) or plan_graph operation=cancel_graph / control.cancel_task (graph-bound).",
|
|
55
55
|
"- When the user enables Plan Mode and no active graph exists, create a plan_graph before performing side effects.",
|
|
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
|
+
"- When the last plan node finishes (graph_terminal), Runtime archives and clears the active plan, exits auto Plan Mode, and returns a completion summary — narrate that summary to the user. Start a new objective with plan_graph create (create replaces any leftover graph); do not patch a finished plan for a new goal.",
|
|
57
58
|
"- 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
59
|
"- 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
60
|
"- 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.",
|
|
@@ -131,7 +131,8 @@ function shouldAutoContinuePlan(executionState = null) {
|
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Internal reminder injected by runtime when the model ends a turn while the
|
|
134
|
-
* plan is still waiting on a task.
|
|
134
|
+
* plan is still waiting on a task. Must NOT reuse the User reminder label —
|
|
135
|
+
* that is reserved for real mid-run user text and pollutes transcript/TUI.
|
|
135
136
|
*/
|
|
136
137
|
function buildPlanAutoContinueReminder(executionState = null) {
|
|
137
138
|
const waiting = executionState
|
|
@@ -140,14 +141,17 @@ function buildPlanAutoContinueReminder(executionState = null) {
|
|
|
140
141
|
? executionState.planGraph.waitingFor
|
|
141
142
|
: null;
|
|
142
143
|
if (!waiting || !waiting.id) return "";
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
{
|
|
150
|
-
|
|
144
|
+
const label = waiting.title || waiting.objective || waiting.reason || waiting.id;
|
|
145
|
+
const lines = [
|
|
146
|
+
"Runtime wake (not a user message): active plan is still waiting.",
|
|
147
|
+
"Continue serving it now via plan_graph "
|
|
148
|
+
+ "(expand_node, control.start_task, or control.complete_task as appropriate). "
|
|
149
|
+
+ "Do not end the turn with text only while this node is waiting.",
|
|
150
|
+
`Prefer serving the current waiting ${waiting.type || "node"}: ${waiting.id}`
|
|
151
|
+
+ (label && label !== waiting.id ? ` — ${label}` : "")
|
|
152
|
+
+ ".",
|
|
153
|
+
];
|
|
154
|
+
return lines.join("\n");
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
module.exports = {
|
|
@@ -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
|
+
};
|