u-foo 2.5.11 → 2.5.13
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/agents/controller/ufooAgent.js +27 -1
- package/src/agents/internal/internalRunner.js +44 -6
- package/src/agents/prompts/native/environment.js +11 -0
- package/src/agents/prompts/native/ufoo.js +1 -0
- package/src/agents/providers/upstreamTransport.js +31 -1
- package/src/app/chat/dashboardView.js +7 -1
- package/src/code/agent.js +18 -1
- package/src/code/nativeRunner.js +169 -2
- package/src/code/repl.js +33 -3
- package/src/code/usageStore.js +101 -0
- package/src/ui/ink/DashboardBar.js +7 -1
package/package.json
CHANGED
|
@@ -640,6 +640,31 @@ function stripMarkdownFence(text = "") {
|
|
|
640
640
|
return raw;
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
+
function toNonNegativeInt(value) {
|
|
644
|
+
const num = Number(value);
|
|
645
|
+
if (!Number.isFinite(num) || num < 0) return 0;
|
|
646
|
+
return Math.floor(num);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Maps provider usage payloads (OpenAI chat, Anthropic messages, Codex responses)
|
|
650
|
+
// onto the metric fields read by extractModelMetrics in loopRuntime.js.
|
|
651
|
+
function extractUsageTokens(usage = null) {
|
|
652
|
+
const item = usage && typeof usage === "object" ? usage : {};
|
|
653
|
+
return {
|
|
654
|
+
input_tokens: toNonNegativeInt(item.input_tokens || item.prompt_tokens),
|
|
655
|
+
output_tokens: toNonNegativeInt(item.output_tokens || item.completion_tokens),
|
|
656
|
+
cache_read_tokens: toNonNegativeInt(
|
|
657
|
+
item.cache_read_tokens
|
|
658
|
+
|| item.cache_read_input_tokens
|
|
659
|
+
|| item.cached_input_tokens
|
|
660
|
+
|| (item.input_tokens_details && item.input_tokens_details.cached_tokens)
|
|
661
|
+
),
|
|
662
|
+
cache_creation_tokens: toNonNegativeInt(
|
|
663
|
+
item.cache_creation_tokens || item.cache_creation_input_tokens
|
|
664
|
+
),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
643
668
|
async function runNativeRouterCall({
|
|
644
669
|
projectRoot,
|
|
645
670
|
prompt,
|
|
@@ -708,7 +733,7 @@ async function runUfooAgent({
|
|
|
708
733
|
console.error(`[ufoo-agent] native provider failed: ${res.error || "unknown error"}`);
|
|
709
734
|
return { ok: false, error: res.error };
|
|
710
735
|
} else {
|
|
711
|
-
res = { ok: true, output: res.output, sessionId: "", provider: res.provider, model: res.model };
|
|
736
|
+
res = { ok: true, output: res.output, sessionId: "", provider: res.provider, model: res.model, usage: res.usage };
|
|
712
737
|
}
|
|
713
738
|
}
|
|
714
739
|
|
|
@@ -746,6 +771,7 @@ async function runUfooAgent({
|
|
|
746
771
|
ok: true,
|
|
747
772
|
payload,
|
|
748
773
|
meta: {
|
|
774
|
+
...extractUsageTokens(res.usage),
|
|
749
775
|
memory_prefix_tokens: memoryPrefixResult.estimated_tokens || 0,
|
|
750
776
|
cache_semistatic_hit: memoryPrefixResult.cache_semistatic_hit || 0,
|
|
751
777
|
cache_semistatic_miss: memoryPrefixResult.cache_semistatic_miss || 0,
|
|
@@ -327,7 +327,7 @@ async function handleEvent(
|
|
|
327
327
|
};
|
|
328
328
|
|
|
329
329
|
if (threadRuntime && threadRuntime.enabled && threadRuntime.thread) {
|
|
330
|
-
|
|
330
|
+
return handleThreadedEvent({
|
|
331
331
|
agentType,
|
|
332
332
|
provider,
|
|
333
333
|
publisher,
|
|
@@ -338,7 +338,6 @@ async function handleEvent(
|
|
|
338
338
|
threadRuntime,
|
|
339
339
|
tracker,
|
|
340
340
|
});
|
|
341
|
-
return;
|
|
342
341
|
}
|
|
343
342
|
|
|
344
343
|
const errorText = `[internal:${agentType}] error: no thread runtime available for provider ${provider}; cliRunner fallback has been removed`;
|
|
@@ -374,6 +373,31 @@ function summarizeThreadToolCall(event = {}) {
|
|
|
374
373
|
return [name, compactToolDetail(detail)].filter(Boolean).join(" · ");
|
|
375
374
|
}
|
|
376
375
|
|
|
376
|
+
function toNonNegativeInt(value) {
|
|
377
|
+
const num = Number(value);
|
|
378
|
+
if (!Number.isFinite(num) || num < 0) return 0;
|
|
379
|
+
return Math.floor(num);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Normalizes provider usage payloads (claude/codex thread events) onto the
|
|
383
|
+
// metric fields read by extractModelMetrics in agents/controller/loopRuntime.js.
|
|
384
|
+
function normalizeTurnUsage(usage = null) {
|
|
385
|
+
const item = usage && typeof usage === "object" ? usage : {};
|
|
386
|
+
return {
|
|
387
|
+
input_tokens: toNonNegativeInt(item.input_tokens || item.prompt_tokens),
|
|
388
|
+
output_tokens: toNonNegativeInt(item.output_tokens || item.completion_tokens),
|
|
389
|
+
cache_read_tokens: toNonNegativeInt(
|
|
390
|
+
item.cache_read_tokens
|
|
391
|
+
|| item.cache_read_input_tokens
|
|
392
|
+
|| item.cached_input_tokens
|
|
393
|
+
|| (item.input_tokens_details && item.input_tokens_details.cached_tokens)
|
|
394
|
+
),
|
|
395
|
+
cache_creation_tokens: toNonNegativeInt(
|
|
396
|
+
item.cache_creation_tokens || item.cache_creation_input_tokens
|
|
397
|
+
),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
377
401
|
async function handleThreadedEvent({
|
|
378
402
|
agentType,
|
|
379
403
|
provider,
|
|
@@ -387,6 +411,8 @@ async function handleThreadedEvent({
|
|
|
387
411
|
}) {
|
|
388
412
|
try {
|
|
389
413
|
const plainReplyParts = [];
|
|
414
|
+
let turnUsage = null;
|
|
415
|
+
let stopReason = "";
|
|
390
416
|
if (tracker && typeof tracker.notifyTurnStart === "function") {
|
|
391
417
|
tracker.notifyTurnStart();
|
|
392
418
|
}
|
|
@@ -409,6 +435,11 @@ async function handleThreadedEvent({
|
|
|
409
435
|
if (streamToPublisher && summary) {
|
|
410
436
|
emitStreamDelta(`\nTool: ${summary}\n`);
|
|
411
437
|
}
|
|
438
|
+
} else if (event.type === "usage" && event.usage) {
|
|
439
|
+
turnUsage = normalizeTurnUsage(event.usage);
|
|
440
|
+
} else if (event.type === "turn_completed") {
|
|
441
|
+
if (event.usage) turnUsage = normalizeTurnUsage(event.usage);
|
|
442
|
+
if (event.stopReason) stopReason = String(event.stopReason);
|
|
412
443
|
} else if (event.type === "turn_failed") {
|
|
413
444
|
throw new Error(event.error || `thread turn failed for ${agentType}`);
|
|
414
445
|
}
|
|
@@ -418,15 +449,21 @@ async function handleThreadedEvent({
|
|
|
418
449
|
}
|
|
419
450
|
|
|
420
451
|
if (streamToPublisher) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
);
|
|
452
|
+
const doneEnvelope = { stream: true, done: true, reason: "complete" };
|
|
453
|
+
if (turnUsage) doneEnvelope.usage = turnUsage;
|
|
454
|
+
busSender.enqueue(publisher, JSON.stringify(doneEnvelope));
|
|
425
455
|
} else {
|
|
426
456
|
const reply = plainReplyParts.join("").trim();
|
|
427
457
|
if (reply) busSender.enqueue(publisher, reply);
|
|
428
458
|
}
|
|
429
459
|
await busSender.flush();
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
meta: {
|
|
463
|
+
...(turnUsage || normalizeTurnUsage(null)),
|
|
464
|
+
stop_reason: stopReason,
|
|
465
|
+
},
|
|
466
|
+
};
|
|
430
467
|
} catch (err) {
|
|
431
468
|
if (threadRuntime && typeof threadRuntime.rebuildThread === "function") {
|
|
432
469
|
await threadRuntime.rebuildThread();
|
|
@@ -450,6 +487,7 @@ async function handleThreadedEvent({
|
|
|
450
487
|
busSender.enqueue(publisher, errorText);
|
|
451
488
|
}
|
|
452
489
|
await busSender.flush();
|
|
490
|
+
return { ok: false, error: errorText };
|
|
453
491
|
}
|
|
454
492
|
}
|
|
455
493
|
|
|
@@ -44,6 +44,17 @@ function getEnvironmentSection({ workspaceRoot = "", model = "", provider = "" }
|
|
|
44
44
|
if (provider) lines.push(`Provider: ${provider}`);
|
|
45
45
|
if (model) lines.push(`Model: ${model}`);
|
|
46
46
|
|
|
47
|
+
// Tell the model who it is on the bus. Without this, the only identities
|
|
48
|
+
// it ever sees are other agents' records in shared context — and it
|
|
49
|
+
// adopts them (observed in the wild: ucode-3 introducing itself as
|
|
50
|
+
// claude-6, then accepting a wrong name from the user).
|
|
51
|
+
const subscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
|
|
52
|
+
const nickname = String(process.env.UFOO_NICKNAME || "").trim();
|
|
53
|
+
if (subscriberId || nickname) {
|
|
54
|
+
const label = nickname ? `${subscriberId || "unknown"} (nickname: ${nickname})` : subscriberId;
|
|
55
|
+
lines.push(`Bus identity: ${label}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
return `# Environment\n${lines.map((l) => ` - ${l}`).join("\n")}`;
|
|
48
59
|
}
|
|
49
60
|
|
|
@@ -4,6 +4,7 @@ function getUfooIntegrationSection() {
|
|
|
4
4
|
return `# ufoo integration
|
|
5
5
|
|
|
6
6
|
Participate in multi-agent coordination through the ufoo bus/context system:
|
|
7
|
+
- Shared context, decisions, and memory are records written by OTHER agents. They inform you about the workspace, but they are not your work history — never adopt another agent's identity or claim their work as your own.
|
|
7
8
|
- Respect shared context decisions. The default is no new decision; only append one for important, plan-level choices that constrain future work, and keep durable project facts out of decisions.
|
|
8
9
|
- Use shared memory for durable project facts. Read existing memory before writing new memory; do not use it for transient task state.
|
|
9
10
|
- Support launch/close/resume/inject flows managed by ufoo daemon.
|
|
@@ -69,6 +69,29 @@ function buildOpenAiChatRequest({
|
|
|
69
69
|
return request;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Anthropic prompt caching allows up to 4 cache_control breakpoints; the
|
|
73
|
+
// system prompt is a stable prefix, so it is always marked, and once there
|
|
74
|
+
// is real history the last user message is marked too so follow-up turns
|
|
75
|
+
// reuse the cached conversation prefix.
|
|
76
|
+
const ANTHROPIC_CACHE_CONTROL = { type: "ephemeral" };
|
|
77
|
+
const ANTHROPIC_CACHE_MIN_HISTORY = 3;
|
|
78
|
+
|
|
79
|
+
function withAnthropicCacheControl(content) {
|
|
80
|
+
if (Array.isArray(content)) {
|
|
81
|
+
if (!content.length) return content;
|
|
82
|
+
return content.map((block, index) => (
|
|
83
|
+
index === content.length - 1 && block && typeof block === "object"
|
|
84
|
+
? { ...block, cache_control: { ...ANTHROPIC_CACHE_CONTROL } }
|
|
85
|
+
: block
|
|
86
|
+
));
|
|
87
|
+
}
|
|
88
|
+
return [{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: String(content || ""),
|
|
91
|
+
cache_control: { ...ANTHROPIC_CACHE_CONTROL },
|
|
92
|
+
}];
|
|
93
|
+
}
|
|
94
|
+
|
|
72
95
|
function buildAnthropicMessagesRequest({
|
|
73
96
|
model = "",
|
|
74
97
|
systemPrompt = "",
|
|
@@ -82,13 +105,20 @@ function buildAnthropicMessagesRequest({
|
|
|
82
105
|
if (!requestMessages.length) {
|
|
83
106
|
requestMessages.push({ role: "user", content: String(prompt || "") });
|
|
84
107
|
}
|
|
108
|
+
if (requestMessages.length >= ANTHROPIC_CACHE_MIN_HISTORY) {
|
|
109
|
+
for (let i = requestMessages.length - 1; i >= 0; i -= 1) {
|
|
110
|
+
if (requestMessages[i].role !== "user") continue;
|
|
111
|
+
requestMessages[i].content = withAnthropicCacheControl(requestMessages[i].content);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
85
115
|
const request = {
|
|
86
116
|
model: String(model || "").trim(),
|
|
87
117
|
max_tokens: maxTokens,
|
|
88
118
|
messages: requestMessages,
|
|
89
119
|
temperature,
|
|
90
120
|
};
|
|
91
|
-
if (systemPrompt) request.system = systemPrompt;
|
|
121
|
+
if (systemPrompt) request.system = withAnthropicCacheControl(String(systemPrompt));
|
|
92
122
|
if (Array.isArray(tools) && tools.length > 0) {
|
|
93
123
|
request.tools = tools.slice();
|
|
94
124
|
}
|
|
@@ -72,6 +72,7 @@ function formatLoopSummary(loopSummary) {
|
|
|
72
72
|
const rounds = Number(loopSummary.rounds) || 0;
|
|
73
73
|
const toolCalls = Number(loopSummary.tool_calls) || 0;
|
|
74
74
|
const totalTokens = Number(loopSummary.total_tokens) || 0;
|
|
75
|
+
const inputTokens = Number(loopSummary.input_tokens) || 0;
|
|
75
76
|
const cacheReadTokens = Number(loopSummary.cache_read_tokens) || 0;
|
|
76
77
|
const cacheCreationTokens = Number(loopSummary.cache_creation_tokens) || 0;
|
|
77
78
|
const terminalReason = String(loopSummary.terminal_reason || "").trim();
|
|
@@ -87,7 +88,12 @@ function formatLoopSummary(loopSummary) {
|
|
|
87
88
|
`tok${totalTokens}`,
|
|
88
89
|
];
|
|
89
90
|
if (cacheReadTokens > 0 || cacheCreationTokens > 0) {
|
|
90
|
-
|
|
91
|
+
let cachePart = `cache${cacheReadTokens}/${cacheCreationTokens}`;
|
|
92
|
+
if (cacheReadTokens > 0) {
|
|
93
|
+
const hitRate = Math.round((cacheReadTokens / (cacheReadTokens + inputTokens)) * 100);
|
|
94
|
+
cachePart += `(${hitRate}%)`;
|
|
95
|
+
}
|
|
96
|
+
parts.push(cachePart);
|
|
91
97
|
}
|
|
92
98
|
if (toolDistribution) {
|
|
93
99
|
parts.push(toolDistribution);
|
package/src/code/agent.js
CHANGED
|
@@ -141,6 +141,18 @@ function computeExtendedTimeout(baseTimeoutMs) {
|
|
|
141
141
|
return Math.min(1800000, Math.max(base * 2, base + 120000));
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// Reasoning models routinely blew the old 10min budget across a multi-turn
|
|
145
|
+
// tool loop. Total per-task budget defaults to 30min and can be raised per
|
|
146
|
+
// call, via --timeout-ms, or via UFOO_UCODE_TASK_TIMEOUT_MS.
|
|
147
|
+
const DEFAULT_NL_TASK_TIMEOUT_MS = 1800000;
|
|
148
|
+
|
|
149
|
+
function resolveNlTaskTimeoutMs(value) {
|
|
150
|
+
if (Number.isFinite(value) && value > 0) return Math.max(1000, Math.floor(value));
|
|
151
|
+
const env = Number(process.env.UFOO_UCODE_TASK_TIMEOUT_MS);
|
|
152
|
+
if (Number.isFinite(env) && env > 0) return Math.max(1000, Math.floor(env));
|
|
153
|
+
return DEFAULT_NL_TASK_TIMEOUT_MS;
|
|
154
|
+
}
|
|
155
|
+
|
|
144
156
|
function enrichNativeError(errorMessage = "") {
|
|
145
157
|
const text = String(errorMessage || "").trim();
|
|
146
158
|
if (!text) return "nl task failed";
|
|
@@ -168,6 +180,9 @@ function enrichNativeError(errorMessage = "") {
|
|
|
168
180
|
) {
|
|
169
181
|
return `${text}. Check provider/url/key via /settings ucode show.`;
|
|
170
182
|
}
|
|
183
|
+
if (lower.includes("cli timeout")) {
|
|
184
|
+
return `${text}. Task budget exceeded; raise it with --timeout-ms or UFOO_UCODE_TASK_TIMEOUT_MS.`;
|
|
185
|
+
}
|
|
171
186
|
return text;
|
|
172
187
|
}
|
|
173
188
|
|
|
@@ -394,7 +409,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
394
409
|
state.provider || process.env.UFOO_UCODE_PROVIDER || ""
|
|
395
410
|
);
|
|
396
411
|
const model = String(state.model || process.env.UFOO_UCODE_MODEL || "").trim();
|
|
397
|
-
const timeoutMs =
|
|
412
|
+
const timeoutMs = resolveNlTaskTimeoutMs(state.timeoutMs);
|
|
398
413
|
let streamed = false;
|
|
399
414
|
let streamLastChar = "";
|
|
400
415
|
let toolEventsThisAttempt = 0;
|
|
@@ -715,6 +730,8 @@ module.exports = {
|
|
|
715
730
|
resolvePlannerProvider,
|
|
716
731
|
extractJsonSummary,
|
|
717
732
|
enrichNativeError,
|
|
733
|
+
resolveNlTaskTimeoutMs,
|
|
734
|
+
DEFAULT_NL_TASK_TIMEOUT_MS,
|
|
718
735
|
resolveUcodeProviderModel,
|
|
719
736
|
buildSessionSnapshotFromState,
|
|
720
737
|
persistSessionState,
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
resolveKimiUpstreamCredentials,
|
|
6
6
|
} = require("../agents/providers/credentials/kimi");
|
|
7
7
|
const { runToolCall } = require("./dispatch");
|
|
8
|
+
const { appendUsageRecord } = require("./usageStore");
|
|
8
9
|
const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
|
|
9
10
|
const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
|
|
10
11
|
const { getEditToolDescription } = require("../agents/prompts/native/toolDescriptions/edit");
|
|
@@ -25,6 +26,11 @@ const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 5;
|
|
|
25
26
|
// via UFOO_UCODE_MAX_TOKENS (positive integer).
|
|
26
27
|
const DEFAULT_OPENAI_MAX_TOKENS = 131072;
|
|
27
28
|
const DEFAULT_ANTHROPIC_MAX_TOKENS = 64000;
|
|
29
|
+
// Prompt caching is GA on the current Messages API: cache_control blocks need
|
|
30
|
+
// no anthropic-beta header. Kept as a constant so the marker shape stays in
|
|
31
|
+
// one place (system block + last history message, 2 of the 4 allowed
|
|
32
|
+
// breakpoints).
|
|
33
|
+
const ANTHROPIC_CACHE_CONTROL = Object.freeze({ type: "ephemeral" });
|
|
28
34
|
|
|
29
35
|
function nowMs() {
|
|
30
36
|
return Date.now();
|
|
@@ -53,6 +59,58 @@ function resolveMaxTokens(fallback) {
|
|
|
53
59
|
return normalizePositiveInt(process.env.UFOO_UCODE_MAX_TOKENS, fallback);
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
function toUsageInt(value) {
|
|
63
|
+
const parsed = Number(value);
|
|
64
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 0;
|
|
65
|
+
return Math.floor(parsed);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createUsageTotals() {
|
|
69
|
+
return {
|
|
70
|
+
turns: 0,
|
|
71
|
+
input: 0,
|
|
72
|
+
output: 0,
|
|
73
|
+
cacheRead: 0,
|
|
74
|
+
cacheCreation: 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addUsageTotals(totals, usage = null) {
|
|
79
|
+
if (!usage || typeof usage !== "object") return totals;
|
|
80
|
+
totals.input += toUsageInt(usage.input);
|
|
81
|
+
totals.output += toUsageInt(usage.output);
|
|
82
|
+
totals.cacheRead += toUsageInt(usage.cacheRead);
|
|
83
|
+
totals.cacheCreation += toUsageInt(usage.cacheCreation);
|
|
84
|
+
return totals;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// OpenAI-compatible streams end with one usage chunk carrying whole-turn
|
|
88
|
+
// totals (prompt_tokens_details.cached_tokens counts the cache hits).
|
|
89
|
+
function readOpenAiUsage(raw) {
|
|
90
|
+
if (!raw || typeof raw !== "object") return null;
|
|
91
|
+
const details = raw.prompt_tokens_details && typeof raw.prompt_tokens_details === "object"
|
|
92
|
+
? raw.prompt_tokens_details
|
|
93
|
+
: {};
|
|
94
|
+
return {
|
|
95
|
+
input: toUsageInt(raw.prompt_tokens),
|
|
96
|
+
output: toUsageInt(raw.completion_tokens),
|
|
97
|
+
cacheRead: toUsageInt(details.cached_tokens),
|
|
98
|
+
cacheCreation: 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Anthropic reports input/cache tokens once on message_start; output tokens
|
|
103
|
+
// arrive per message_delta. The non-streaming body carries the full totals.
|
|
104
|
+
function readAnthropicUsage(raw, { includeOutput = false } = {}) {
|
|
105
|
+
if (!raw || typeof raw !== "object") return null;
|
|
106
|
+
return {
|
|
107
|
+
input: toUsageInt(raw.input_tokens),
|
|
108
|
+
output: includeOutput ? toUsageInt(raw.output_tokens) : 0,
|
|
109
|
+
cacheRead: toUsageInt(raw.cache_read_input_tokens),
|
|
110
|
+
cacheCreation: toUsageInt(raw.cache_creation_input_tokens),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
56
114
|
function enforceNativeToolBudget({
|
|
57
115
|
toolCallsExecuted = 0,
|
|
58
116
|
toolErrors = 0,
|
|
@@ -576,6 +634,8 @@ async function runOpenAiLikeTurn({
|
|
|
576
634
|
tools: buildCoreToolSpecs(),
|
|
577
635
|
tool_choice: "auto",
|
|
578
636
|
stream: true,
|
|
637
|
+
// Ask for the terminal usage chunk so token/cache accounting works.
|
|
638
|
+
stream_options: { include_usage: true },
|
|
579
639
|
// Kimi k3 rejects any temperature other than 1.
|
|
580
640
|
temperature: normalizeProvider(provider) === "kimi" ? 1 : 0,
|
|
581
641
|
};
|
|
@@ -592,6 +652,7 @@ async function runOpenAiLikeTurn({
|
|
|
592
652
|
let responseText = "";
|
|
593
653
|
let nextSyntheticIndex = 0;
|
|
594
654
|
let lastSyntheticIndex = -1;
|
|
655
|
+
let streamUsage = null;
|
|
595
656
|
|
|
596
657
|
return runSseRequest({
|
|
597
658
|
url,
|
|
@@ -612,12 +673,18 @@ async function runOpenAiLikeTurn({
|
|
|
612
673
|
return {
|
|
613
674
|
text,
|
|
614
675
|
toolCalls,
|
|
676
|
+
usage: readOpenAiUsage(data && data.usage),
|
|
615
677
|
};
|
|
616
678
|
},
|
|
617
679
|
onEvent: ({ data }) => {
|
|
618
680
|
const chunk = parseJsonSafe(data, null);
|
|
619
681
|
if (!chunk || typeof chunk !== "object") return;
|
|
620
682
|
|
|
683
|
+
// The usage chunk carries empty choices, so read it before the
|
|
684
|
+
// choice guard below; latest wins (it reports whole-turn totals).
|
|
685
|
+
const chunkUsage = readOpenAiUsage(chunk.usage);
|
|
686
|
+
if (chunkUsage) streamUsage = chunkUsage;
|
|
687
|
+
|
|
621
688
|
const choice = chunk.choices && chunk.choices[0] ? chunk.choices[0] : null;
|
|
622
689
|
if (!choice || typeof choice !== "object") return;
|
|
623
690
|
|
|
@@ -695,6 +762,8 @@ async function runOpenAiLikeTurn({
|
|
|
695
762
|
const fallbackBlock = parseSseDataBlock(rawBuffer);
|
|
696
763
|
if (fallbackBlock && fallbackBlock !== "[DONE]") {
|
|
697
764
|
const chunk = parseJsonSafe(fallbackBlock, null);
|
|
765
|
+
const tailUsage = readOpenAiUsage(chunk && chunk.usage);
|
|
766
|
+
if (tailUsage) streamUsage = tailUsage;
|
|
698
767
|
const choice = chunk && chunk.choices && chunk.choices[0] ? chunk.choices[0] : null;
|
|
699
768
|
if (choice && choice.delta && typeof choice.delta.content === "string" && choice.delta.content) {
|
|
700
769
|
responseText += choice.delta.content;
|
|
@@ -709,6 +778,7 @@ async function runOpenAiLikeTurn({
|
|
|
709
778
|
toolCalls: Array.from(toolCallMap.entries())
|
|
710
779
|
.sort((a, b) => a[0] - b[0])
|
|
711
780
|
.map((entry) => entry[1]),
|
|
781
|
+
usage: streamUsage,
|
|
712
782
|
}),
|
|
713
783
|
});
|
|
714
784
|
}
|
|
@@ -751,6 +821,48 @@ function extractAnthropicToolCalls(content = []) {
|
|
|
751
821
|
}));
|
|
752
822
|
}
|
|
753
823
|
|
|
824
|
+
// Mark the newest message with a cache breakpoint so the append-only history
|
|
825
|
+
// prefix is served from the prompt cache. The payload gets a copy: stamping
|
|
826
|
+
// cache_control onto the shared history array would leave stale breakpoints
|
|
827
|
+
// behind as later turns append, eventually exceeding the 4-breakpoint limit.
|
|
828
|
+
function withAnthropicCacheBreakpoint(messages = []) {
|
|
829
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
830
|
+
const copy = messages.slice();
|
|
831
|
+
const lastIndex = copy.length - 1;
|
|
832
|
+
const last = copy[lastIndex];
|
|
833
|
+
if (!last || typeof last !== "object" || Array.isArray(last)) return copy;
|
|
834
|
+
if (typeof last.content === "string") {
|
|
835
|
+
if (!last.content) return copy;
|
|
836
|
+
copy[lastIndex] = {
|
|
837
|
+
...last,
|
|
838
|
+
content: [
|
|
839
|
+
{
|
|
840
|
+
type: "text",
|
|
841
|
+
text: last.content,
|
|
842
|
+
cache_control: { ...ANTHROPIC_CACHE_CONTROL },
|
|
843
|
+
},
|
|
844
|
+
],
|
|
845
|
+
};
|
|
846
|
+
return copy;
|
|
847
|
+
}
|
|
848
|
+
if (Array.isArray(last.content) && last.content.length > 0) {
|
|
849
|
+
const blocks = last.content.slice();
|
|
850
|
+
const blockIndex = blocks.length - 1;
|
|
851
|
+
const block = blocks[blockIndex];
|
|
852
|
+
if (block && typeof block === "object" && !Array.isArray(block)) {
|
|
853
|
+
blocks[blockIndex] = {
|
|
854
|
+
...block,
|
|
855
|
+
cache_control: { ...ANTHROPIC_CACHE_CONTROL },
|
|
856
|
+
};
|
|
857
|
+
copy[lastIndex] = {
|
|
858
|
+
...last,
|
|
859
|
+
content: blocks,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return copy;
|
|
864
|
+
}
|
|
865
|
+
|
|
754
866
|
async function runAnthropicTurn({
|
|
755
867
|
url = "",
|
|
756
868
|
apiKey = "",
|
|
@@ -766,13 +878,21 @@ async function runAnthropicTurn({
|
|
|
766
878
|
const payload = {
|
|
767
879
|
model,
|
|
768
880
|
max_tokens: resolveMaxTokens(DEFAULT_ANTHROPIC_MAX_TOKENS),
|
|
769
|
-
messages,
|
|
881
|
+
messages: withAnthropicCacheBreakpoint(messages),
|
|
770
882
|
tools: buildAnthropicToolSpecs(),
|
|
771
883
|
stream: true,
|
|
772
884
|
};
|
|
773
885
|
const systemText = String(systemPrompt || "").trim();
|
|
774
886
|
if (systemText) {
|
|
775
|
-
|
|
887
|
+
// Block form with a cache breakpoint; the system prompt is the most
|
|
888
|
+
// stable prefix of every request.
|
|
889
|
+
payload.system = [
|
|
890
|
+
{
|
|
891
|
+
type: "text",
|
|
892
|
+
text: systemText,
|
|
893
|
+
cache_control: { ...ANTHROPIC_CACHE_CONTROL },
|
|
894
|
+
},
|
|
895
|
+
];
|
|
776
896
|
}
|
|
777
897
|
|
|
778
898
|
const headers = {
|
|
@@ -787,6 +907,7 @@ async function runAnthropicTurn({
|
|
|
787
907
|
let responseText = "";
|
|
788
908
|
let nextSyntheticBlockIndex = 0;
|
|
789
909
|
let lastBlockIndex = -1;
|
|
910
|
+
const turnUsage = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
790
911
|
|
|
791
912
|
return runSseRequest({
|
|
792
913
|
url,
|
|
@@ -804,10 +925,12 @@ async function runAnthropicTurn({
|
|
|
804
925
|
if (text && typeof onTextDelta === "function") {
|
|
805
926
|
onTextDelta(text);
|
|
806
927
|
}
|
|
928
|
+
addUsageTotals(turnUsage, readAnthropicUsage(data && data.usage, { includeOutput: true }));
|
|
807
929
|
return {
|
|
808
930
|
text,
|
|
809
931
|
assistantContent: content,
|
|
810
932
|
toolCalls: extractAnthropicToolCalls(content),
|
|
933
|
+
usage: turnUsage,
|
|
811
934
|
};
|
|
812
935
|
},
|
|
813
936
|
onEvent: ({ event, data }) => {
|
|
@@ -821,6 +944,28 @@ async function runAnthropicTurn({
|
|
|
821
944
|
throw new Error(errMsg);
|
|
822
945
|
}
|
|
823
946
|
|
|
947
|
+
if (event === "message_start") {
|
|
948
|
+
const messageUsage = readAnthropicUsage(
|
|
949
|
+
payloadChunk.message && typeof payloadChunk.message === "object"
|
|
950
|
+
? payloadChunk.message.usage
|
|
951
|
+
: null
|
|
952
|
+
);
|
|
953
|
+
if (messageUsage) {
|
|
954
|
+
turnUsage.input = messageUsage.input;
|
|
955
|
+
turnUsage.cacheRead = messageUsage.cacheRead;
|
|
956
|
+
turnUsage.cacheCreation = messageUsage.cacheCreation;
|
|
957
|
+
}
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (event === "message_delta") {
|
|
962
|
+
const deltaUsage = payloadChunk.usage && typeof payloadChunk.usage === "object"
|
|
963
|
+
? payloadChunk.usage
|
|
964
|
+
: {};
|
|
965
|
+
turnUsage.output += toUsageInt(deltaUsage.output_tokens);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
824
969
|
if (event === "content_block_start") {
|
|
825
970
|
let index;
|
|
826
971
|
if (Number.isFinite(payloadChunk.index)) {
|
|
@@ -957,6 +1102,7 @@ async function runAnthropicTurn({
|
|
|
957
1102
|
text: responseText,
|
|
958
1103
|
assistantContent,
|
|
959
1104
|
toolCalls: extractAnthropicToolCalls(assistantContent),
|
|
1105
|
+
usage: turnUsage,
|
|
960
1106
|
};
|
|
961
1107
|
},
|
|
962
1108
|
});
|
|
@@ -1133,6 +1279,7 @@ async function runNativeLoop({
|
|
|
1133
1279
|
let toolCallsExecuted = 0;
|
|
1134
1280
|
let toolErrors = 0;
|
|
1135
1281
|
const toolBudget = resolveNativeToolBudget();
|
|
1282
|
+
const usage = createUsageTotals();
|
|
1136
1283
|
|
|
1137
1284
|
while (true) {
|
|
1138
1285
|
guards.ensureActive();
|
|
@@ -1159,6 +1306,9 @@ async function runNativeLoop({
|
|
|
1159
1306
|
},
|
|
1160
1307
|
});
|
|
1161
1308
|
|
|
1309
|
+
usage.turns += 1;
|
|
1310
|
+
addUsageTotals(usage, turnResult && turnResult.usage);
|
|
1311
|
+
|
|
1162
1312
|
const toolCalls = transport.getToolCalls(turnResult);
|
|
1163
1313
|
|
|
1164
1314
|
if (toolCalls.length === 0) {
|
|
@@ -1172,6 +1322,7 @@ async function runNativeLoop({
|
|
|
1172
1322
|
streamed,
|
|
1173
1323
|
toolCallsExecuted,
|
|
1174
1324
|
messages,
|
|
1325
|
+
usage,
|
|
1175
1326
|
};
|
|
1176
1327
|
}
|
|
1177
1328
|
|
|
@@ -1182,6 +1333,7 @@ async function runNativeLoop({
|
|
|
1182
1333
|
streamed,
|
|
1183
1334
|
toolCallsExecuted,
|
|
1184
1335
|
messages,
|
|
1336
|
+
usage,
|
|
1185
1337
|
};
|
|
1186
1338
|
}
|
|
1187
1339
|
|
|
@@ -1312,12 +1464,27 @@ async function runNativeAgentTask({
|
|
|
1312
1464
|
: ""
|
|
1313
1465
|
);
|
|
1314
1466
|
|
|
1467
|
+
const usage = runResult.usage && typeof runResult.usage === "object"
|
|
1468
|
+
? runResult.usage
|
|
1469
|
+
: createUsageTotals();
|
|
1470
|
+
appendUsageRecord(workspaceRoot, {
|
|
1471
|
+
sessionId: nextSessionId,
|
|
1472
|
+
model: runtime.model,
|
|
1473
|
+
provider: runtime.provider,
|
|
1474
|
+
turns: usage.turns,
|
|
1475
|
+
input: usage.input,
|
|
1476
|
+
output: usage.output,
|
|
1477
|
+
cacheRead: usage.cacheRead,
|
|
1478
|
+
cacheCreation: usage.cacheCreation,
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1315
1481
|
return {
|
|
1316
1482
|
ok: true,
|
|
1317
1483
|
error: "",
|
|
1318
1484
|
output: outputText,
|
|
1319
1485
|
messages: cloneMessageList(runResult.messages),
|
|
1320
1486
|
sessionId: nextSessionId,
|
|
1487
|
+
usage,
|
|
1321
1488
|
// The loop marks streamed=true whenever it receives a stream callback;
|
|
1322
1489
|
// only report it when the caller actually registered one.
|
|
1323
1490
|
streamed: Boolean(runResult.streamed) && typeof onStreamDelta === "function",
|
package/src/code/repl.js
CHANGED
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
getPendingBusCount,
|
|
22
22
|
shouldAutoConsumeBus,
|
|
23
23
|
} = require("./busConsumer");
|
|
24
|
+
const { summarizeSessionUsage } = require("./usageStore");
|
|
24
25
|
|
|
25
26
|
function printPrompt(stdout = process.stdout) {
|
|
26
27
|
stdout.write("> ");
|
|
@@ -74,6 +75,20 @@ function extractAgentNickname(agentId = "") {
|
|
|
74
75
|
return base;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
function formatSessionUsageStatus(summary = {}) {
|
|
79
|
+
const source = summary && typeof summary === "object" ? summary : {};
|
|
80
|
+
const input = Number(source.input) || 0;
|
|
81
|
+
const output = Number(source.output) || 0;
|
|
82
|
+
const cacheRead = Number(source.cacheRead) || 0;
|
|
83
|
+
const cacheCreation = Number(source.cacheCreation) || 0;
|
|
84
|
+
const denominator = cacheRead + input;
|
|
85
|
+
const hitRate = denominator > 0 ? (cacheRead / denominator) * 100 : 0;
|
|
86
|
+
return [
|
|
87
|
+
`Session tokens: input=${input} output=${output} cache_read=${cacheRead} cache_creation=${cacheCreation}`,
|
|
88
|
+
`Cache hit rate: ${hitRate.toFixed(1)}% (cache_read/(cache_read+input))`,
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
78
93
|
const text = normalizeLine(line);
|
|
79
94
|
if (!text) return { kind: "empty" };
|
|
@@ -86,6 +101,7 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
86
101
|
" help",
|
|
87
102
|
" exit|quit",
|
|
88
103
|
" ubus|/ubus",
|
|
104
|
+
" status|/status",
|
|
89
105
|
" skills [list]",
|
|
90
106
|
" skills show <name>",
|
|
91
107
|
" bg|/bg <task>",
|
|
@@ -107,6 +123,11 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
107
123
|
kind: "ubus",
|
|
108
124
|
};
|
|
109
125
|
}
|
|
126
|
+
if (text === "status" || text === "/status") {
|
|
127
|
+
return {
|
|
128
|
+
kind: "status",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
110
131
|
const skillsMatch = text.match(/^(?:\/skills|skills)(?:\s+(.*))?$/i);
|
|
111
132
|
if (skillsMatch) {
|
|
112
133
|
const args = String(skillsMatch[1] || "").trim().split(/\s+/).filter(Boolean);
|
|
@@ -228,7 +249,7 @@ async function runUcodeCoreAgent({
|
|
|
228
249
|
appendSystemPrompt = "",
|
|
229
250
|
systemPrompt = "",
|
|
230
251
|
sessionId = "",
|
|
231
|
-
timeoutMs =
|
|
252
|
+
timeoutMs = 0,
|
|
232
253
|
jsonOutput = false,
|
|
233
254
|
forceTui = false,
|
|
234
255
|
disableTui = false,
|
|
@@ -240,6 +261,7 @@ async function runUcodeCoreAgent({
|
|
|
240
261
|
formatNlResult,
|
|
241
262
|
persistSessionState,
|
|
242
263
|
resumeSessionState,
|
|
264
|
+
resolveNlTaskTimeoutMs,
|
|
243
265
|
resolveUcodeProviderModel,
|
|
244
266
|
runNaturalLanguageTask,
|
|
245
267
|
} = require("./agent");
|
|
@@ -263,7 +285,7 @@ async function runUcodeCoreAgent({
|
|
|
263
285
|
}),
|
|
264
286
|
nlMessages: [],
|
|
265
287
|
sessionId: resolveSessionId(String(sessionId || "").trim()),
|
|
266
|
-
timeoutMs,
|
|
288
|
+
timeoutMs: resolveNlTaskTimeoutMs(Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : NaN),
|
|
267
289
|
jsonOutput,
|
|
268
290
|
};
|
|
269
291
|
persistSessionState(state);
|
|
@@ -410,6 +432,13 @@ async function runUcodeCoreAgent({
|
|
|
410
432
|
if (result.kind === "help" || result.kind === "tool" || result.kind === "skills" || result.kind === "error") {
|
|
411
433
|
stdout.write(`${result.output}\n`);
|
|
412
434
|
}
|
|
435
|
+
if (result.kind === "status") {
|
|
436
|
+
const usageSummary = summarizeSessionUsage({
|
|
437
|
+
workspaceRoot: runtimeWorkspace,
|
|
438
|
+
sessionId: state.sessionId,
|
|
439
|
+
});
|
|
440
|
+
stdout.write(`${formatSessionUsageStatus(usageSummary)}\n`);
|
|
441
|
+
}
|
|
413
442
|
if (result.kind === "ubus") {
|
|
414
443
|
const ubusResult = await runUbusCommand(state, {
|
|
415
444
|
workspaceRoot: runtimeWorkspace,
|
|
@@ -542,7 +571,7 @@ function parseAgentArgs(argv = []) {
|
|
|
542
571
|
appendSystemPrompt: "",
|
|
543
572
|
systemPrompt: "",
|
|
544
573
|
sessionId: "",
|
|
545
|
-
timeoutMs:
|
|
574
|
+
timeoutMs: 0,
|
|
546
575
|
jsonOutput: false,
|
|
547
576
|
forceTui: false,
|
|
548
577
|
disableTui: false,
|
|
@@ -607,4 +636,5 @@ module.exports = {
|
|
|
607
636
|
runSingleCommand,
|
|
608
637
|
extractAgentNickname,
|
|
609
638
|
parseAgentArgs,
|
|
639
|
+
formatSessionUsageStatus,
|
|
610
640
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function getUsageFilePath(workspaceRoot = process.cwd()) {
|
|
5
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
6
|
+
return path.join(root, ".ufoo", "agent", "ucode", "usage.jsonl");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toUsageCount(value) {
|
|
10
|
+
const parsed = Number(value);
|
|
11
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 0;
|
|
12
|
+
return Math.floor(parsed);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildUsageRecord(input = {}) {
|
|
16
|
+
const source = input && typeof input === "object" ? input : {};
|
|
17
|
+
return {
|
|
18
|
+
ts: String(source.ts || "").trim() || new Date().toISOString(),
|
|
19
|
+
sessionId: String(source.sessionId || "").trim(),
|
|
20
|
+
model: String(source.model || "").trim(),
|
|
21
|
+
provider: String(source.provider || "").trim(),
|
|
22
|
+
turns: toUsageCount(source.turns),
|
|
23
|
+
input: toUsageCount(source.input),
|
|
24
|
+
output: toUsageCount(source.output),
|
|
25
|
+
cacheRead: toUsageCount(source.cacheRead),
|
|
26
|
+
cacheCreation: toUsageCount(source.cacheCreation),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function appendUsageRecord(workspaceRoot = process.cwd(), record = {}) {
|
|
31
|
+
const row = buildUsageRecord(record);
|
|
32
|
+
const filePath = getUsageFilePath(workspaceRoot);
|
|
33
|
+
try {
|
|
34
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
35
|
+
fs.appendFileSync(filePath, `${JSON.stringify(row)}\n`, "utf8");
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
error: "",
|
|
39
|
+
filePath,
|
|
40
|
+
record: row,
|
|
41
|
+
};
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// Usage accounting is observability only: never let a write failure
|
|
44
|
+
// break the agent loop.
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: err && err.message ? err.message : "failed to append usage",
|
|
48
|
+
filePath,
|
|
49
|
+
record: row,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createUsageSummary() {
|
|
55
|
+
return {
|
|
56
|
+
records: 0,
|
|
57
|
+
turns: 0,
|
|
58
|
+
input: 0,
|
|
59
|
+
output: 0,
|
|
60
|
+
cacheRead: 0,
|
|
61
|
+
cacheCreation: 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function summarizeSessionUsage({ workspaceRoot = process.cwd(), sessionId = "" } = {}) {
|
|
66
|
+
const summary = createUsageSummary();
|
|
67
|
+
const targetSessionId = String(sessionId || "").trim();
|
|
68
|
+
let raw = "";
|
|
69
|
+
try {
|
|
70
|
+
raw = fs.readFileSync(getUsageFilePath(workspaceRoot), "utf8");
|
|
71
|
+
} catch {
|
|
72
|
+
return summary;
|
|
73
|
+
}
|
|
74
|
+
for (const line of String(raw).split(/\r?\n/)) {
|
|
75
|
+
const text = line.trim();
|
|
76
|
+
if (!text) continue;
|
|
77
|
+
let row = null;
|
|
78
|
+
try {
|
|
79
|
+
row = JSON.parse(text);
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
84
|
+
if (targetSessionId && String(row.sessionId || "").trim() !== targetSessionId) continue;
|
|
85
|
+
summary.records += 1;
|
|
86
|
+
summary.turns += toUsageCount(row.turns);
|
|
87
|
+
summary.input += toUsageCount(row.input);
|
|
88
|
+
summary.output += toUsageCount(row.output);
|
|
89
|
+
summary.cacheRead += toUsageCount(row.cacheRead);
|
|
90
|
+
summary.cacheCreation += toUsageCount(row.cacheCreation);
|
|
91
|
+
}
|
|
92
|
+
return summary;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
getUsageFilePath,
|
|
97
|
+
buildUsageRecord,
|
|
98
|
+
appendUsageRecord,
|
|
99
|
+
createUsageSummary,
|
|
100
|
+
summarizeSessionUsage,
|
|
101
|
+
};
|
|
@@ -74,6 +74,7 @@ function formatLoopSummary(loopSummary) {
|
|
|
74
74
|
const rounds = Number(loopSummary.rounds) || 0;
|
|
75
75
|
const toolCalls = Number(loopSummary.tool_calls) || 0;
|
|
76
76
|
const totalTokens = Number(loopSummary.total_tokens) || 0;
|
|
77
|
+
const inputTokens = Number(loopSummary.input_tokens) || 0;
|
|
77
78
|
const cacheReadTokens = Number(loopSummary.cache_read_tokens) || 0;
|
|
78
79
|
const cacheCreationTokens = Number(loopSummary.cache_creation_tokens) || 0;
|
|
79
80
|
const terminalReason = String(loopSummary.terminal_reason || "").trim();
|
|
@@ -81,7 +82,12 @@ function formatLoopSummary(loopSummary) {
|
|
|
81
82
|
if (rounds <= 0 && toolCalls <= 0 && totalTokens <= 0 && !terminalReason && !toolDistribution) return "";
|
|
82
83
|
const parts = [`r${rounds}`, `tc${toolCalls}`, `tok${totalTokens}`];
|
|
83
84
|
if (cacheReadTokens > 0 || cacheCreationTokens > 0) {
|
|
84
|
-
|
|
85
|
+
let cachePart = `cache${cacheReadTokens}/${cacheCreationTokens}`;
|
|
86
|
+
if (cacheReadTokens > 0) {
|
|
87
|
+
const hitRate = Math.round((cacheReadTokens / (cacheReadTokens + inputTokens)) * 100);
|
|
88
|
+
cachePart += `(${hitRate}%)`;
|
|
89
|
+
}
|
|
90
|
+
parts.push(cachePart);
|
|
85
91
|
}
|
|
86
92
|
if (toolDistribution) parts.push(toolDistribution);
|
|
87
93
|
if (terminalReason) parts.push(terminalReason);
|