@keyou007/agent-bot 0.1.24-alpha.3 → 0.1.24-alpha.5
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/CHANGELOG.md +49 -0
- package/README.md +22 -4
- package/README.zh.md +22 -4
- package/dist/acp/AcpSessionManager.js +0 -14
- package/dist/acp/AcpSessionManager.js.map +1 -1
- package/dist/acp/acpTypes.js.map +1 -1
- package/dist/codex/AppServerPermissions.js +34 -0
- package/dist/codex/AppServerPermissions.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +37 -5
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexRuntime.js +37 -13
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ThreadWriterProcess.js +19 -5
- package/dist/codex/ThreadWriterProcess.js.map +1 -1
- package/dist/commands/CommandRouter.js +0 -3
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +80 -64
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/local-files/CsvPreview.js +148 -0
- package/dist/local-files/CsvPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +53 -10
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +125 -14
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +39 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +158 -50
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/docs/technical-reference.md +5 -3
- package/docs/technical-reference.zh.md +4 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +10 -0
- package/src/acp/AcpSessionManager.ts +0 -17
- package/src/acp/acpTypes.ts +0 -1
- package/src/codex/AppServerPermissions.ts +39 -0
- package/src/codex/CodexLocalActivityDetector.ts +34 -4
- package/src/codex/CodexRuntime.ts +44 -17
- package/src/codex/ThreadWriterProcess.ts +17 -9
- package/src/commands/CommandRouter.ts +0 -3
- package/src/feishu/CardRenderer.ts +78 -64
- package/src/local-files/CsvPreview.ts +132 -0
- package/src/local-files/LocalFileViewerServer.ts +54 -11
- package/src/local-files/TurnPreviewPage.ts +128 -15
- package/src/presentation/TurnStateReducer.ts +41 -0
- package/src/presentation/turnViewTypes.ts +4 -0
- package/src/proxy/ProxySessionController.ts +142 -51
- package/src/runtime/types.ts +2 -0
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { enableDiagramFences, DIAGRAM_PREVIEW_CLIENT_SCRIPT, DIAGRAM_PREVIEW_CSS } from "./DiagramPreview.js";
|
|
4
4
|
import MarkdownIt from "markdown-it";
|
|
5
5
|
import { highlightPreviewCode, PREVIEW_SYNTAX_CSS } from "./PreviewSyntaxHighlight.js";
|
|
6
|
-
import type { ToolState } from "../runtime/types.js";
|
|
6
|
+
import type { ApprovalRequest, ToolState } from "../runtime/types.js";
|
|
7
7
|
import type { FileSummary, TurnActivity, TurnReasoningItem, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
8
8
|
import { displayFilePath, displayToolCommand, formatShellCommandForDisplay, toolStatusIcon } from "../feishu/CardRenderer.js";
|
|
9
9
|
import { turnReasoningItems } from "../presentation/turnReasoning.js";
|
|
@@ -41,6 +41,8 @@ const PREVIEW_LABELS = {
|
|
|
41
41
|
error: "错误",
|
|
42
42
|
reasoning: "思考",
|
|
43
43
|
finalAnswer: "最终回答",
|
|
44
|
+
downloadMarkdown: "下载 Markdown",
|
|
45
|
+
copyMarkdown: "复制 Markdown",
|
|
44
46
|
generating: "回答生成中",
|
|
45
47
|
command: "命令",
|
|
46
48
|
result: "结果",
|
|
@@ -51,18 +53,19 @@ const PREVIEW_LABELS = {
|
|
|
51
53
|
noToolDetails: "没有可显示的工具详情。",
|
|
52
54
|
fileChanges: "文件变更",
|
|
53
55
|
model: "模型",
|
|
56
|
+
modelCalls: "模型调用",
|
|
57
|
+
modelCallsDescription: "本轮有效 Token 用量更新次数",
|
|
58
|
+
contextTokens: "上下文",
|
|
54
59
|
tools: "个工具",
|
|
55
60
|
turnTokens: "本轮",
|
|
56
61
|
totalTokens: "总计",
|
|
57
62
|
nonCachedTokens: "非缓存",
|
|
58
|
-
compactionTokens: "压缩",
|
|
59
|
-
compactionAfterTokens: "压缩后",
|
|
60
63
|
characters: "字符",
|
|
61
64
|
truncated: "较早的活动已被运行时截断;本页展示 Agent Bot 当前保存的完整快照。",
|
|
62
65
|
statuses: {
|
|
63
66
|
starting: "正在启动",
|
|
64
67
|
running: "正在处理",
|
|
65
|
-
tool_running: "
|
|
68
|
+
tool_running: "正在处理",
|
|
66
69
|
waiting_for_approval: "等待确认",
|
|
67
70
|
completed: "已完成",
|
|
68
71
|
cancelled: "已停止",
|
|
@@ -82,6 +85,8 @@ const PREVIEW_LABELS = {
|
|
|
82
85
|
error: "Error",
|
|
83
86
|
reasoning: "Reasoning",
|
|
84
87
|
finalAnswer: "Final answer",
|
|
88
|
+
downloadMarkdown: "Download Markdown",
|
|
89
|
+
copyMarkdown: "Copy Markdown",
|
|
85
90
|
generating: "Generating answer",
|
|
86
91
|
command: "Command",
|
|
87
92
|
result: "Result",
|
|
@@ -92,18 +97,19 @@ const PREVIEW_LABELS = {
|
|
|
92
97
|
noToolDetails: "No tool details available.",
|
|
93
98
|
fileChanges: "File changes",
|
|
94
99
|
model: "Model",
|
|
100
|
+
modelCalls: "Model calls",
|
|
101
|
+
modelCallsDescription: "Effective token usage updates in this turn",
|
|
102
|
+
contextTokens: "Context",
|
|
95
103
|
tools: "tools",
|
|
96
104
|
turnTokens: "Turn",
|
|
97
105
|
totalTokens: "Total",
|
|
98
106
|
nonCachedTokens: "Non-cached",
|
|
99
|
-
compactionTokens: "Compaction",
|
|
100
|
-
compactionAfterTokens: "After compaction",
|
|
101
107
|
characters: "characters",
|
|
102
108
|
truncated: "Earlier activities were truncated by the runtime; this page shows the complete snapshot currently saved by Agent Bot.",
|
|
103
109
|
statuses: {
|
|
104
110
|
starting: "Starting",
|
|
105
111
|
running: "Processing",
|
|
106
|
-
tool_running: "
|
|
112
|
+
tool_running: "Processing",
|
|
107
113
|
waiting_for_approval: "Waiting for approval",
|
|
108
114
|
completed: "Completed",
|
|
109
115
|
cancelled: "Stopped",
|
|
@@ -152,7 +158,76 @@ export const TURN_PREVIEW_CLIENT_SCRIPT = DIAGRAM_PREVIEW_CLIENT_SCRIPT + `(() =
|
|
|
152
158
|
loading: isChinese ? "正在加载…" : "Loading…",
|
|
153
159
|
loadFailed: isChinese ? "加载失败,请重试。" : "Could not load details.",
|
|
154
160
|
retry: isChinese ? "重试" : "Retry",
|
|
161
|
+
copied: isChinese ? "已复制 Markdown" : "Markdown copied",
|
|
162
|
+
downloaded: isChinese ? "已开始下载" : "Download started",
|
|
163
|
+
copyFailed: isChinese ? "复制失败,请检查浏览器剪贴板权限后重试。" : "Copy failed. Check clipboard permissions and retry.",
|
|
164
|
+
downloadFailed: isChinese ? "下载失败,请重试。" : "Download failed. Please retry.",
|
|
155
165
|
};
|
|
166
|
+
const copyMarkdownFallback = (text) => {
|
|
167
|
+
const active = document.activeElement;
|
|
168
|
+
const selection = window.getSelection?.();
|
|
169
|
+
const ranges = selection ? Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i).cloneRange()) : [];
|
|
170
|
+
const textarea = document.createElement("textarea");
|
|
171
|
+
textarea.value = text;
|
|
172
|
+
textarea.readOnly = true;
|
|
173
|
+
textarea.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none";
|
|
174
|
+
document.body.append(textarea);
|
|
175
|
+
try {
|
|
176
|
+
textarea.focus({ preventScroll: true });
|
|
177
|
+
textarea.select();
|
|
178
|
+
textarea.setSelectionRange(0, text.length);
|
|
179
|
+
if (!document.execCommand?.("copy")) throw new Error("Clipboard unavailable");
|
|
180
|
+
} finally {
|
|
181
|
+
textarea.remove();
|
|
182
|
+
active?.focus?.({ preventScroll: true });
|
|
183
|
+
if (selection) {
|
|
184
|
+
selection.removeAllRanges();
|
|
185
|
+
for (const range of ranges) selection.addRange(range);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const copyMarkdown = async (text) => {
|
|
190
|
+
if (window.navigator?.clipboard?.writeText) {
|
|
191
|
+
try { await window.navigator.clipboard.writeText(text); return; } catch {}
|
|
192
|
+
}
|
|
193
|
+
copyMarkdownFallback(text);
|
|
194
|
+
};
|
|
195
|
+
const answerActionsInFlight = new WeakSet();
|
|
196
|
+
content.addEventListener("click", async (event) => {
|
|
197
|
+
const button = event.target.closest?.("button[data-answer-action]");
|
|
198
|
+
const section = button?.closest(".final-result[data-answer-markdown]");
|
|
199
|
+
if (!section || !content.contains(section) || answerActionsInFlight.has(button)) return;
|
|
200
|
+
const action = button.dataset.answerAction;
|
|
201
|
+
if (action !== "copy" && action !== "download") return;
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
const feedback = section.querySelector("[data-answer-feedback]");
|
|
204
|
+
answerActionsInFlight.add(button);
|
|
205
|
+
button.disabled = true;
|
|
206
|
+
try {
|
|
207
|
+
const markdown = new TextDecoder("utf-8", { ignoreBOM: true }).decode(Uint8Array.from(atob(section.dataset.answerMarkdown), (char) => char.charCodeAt(0)));
|
|
208
|
+
if (action === "copy") await copyMarkdown(markdown);
|
|
209
|
+
else {
|
|
210
|
+
const url = URL.createObjectURL(new Blob([markdown], { type: "text/markdown;charset=utf-8" }));
|
|
211
|
+
const link = document.createElement("a");
|
|
212
|
+
try {
|
|
213
|
+
link.href = url;
|
|
214
|
+
link.download = section.dataset.answerFilename;
|
|
215
|
+
link.hidden = true;
|
|
216
|
+
document.body.append(link);
|
|
217
|
+
link.click();
|
|
218
|
+
} finally {
|
|
219
|
+
link.remove();
|
|
220
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (feedback) feedback.textContent = action === "copy" ? labels.copied : labels.downloaded;
|
|
224
|
+
} catch {
|
|
225
|
+
if (feedback) feedback.textContent = action === "copy" ? labels.copyFailed : labels.downloadFailed;
|
|
226
|
+
} finally {
|
|
227
|
+
button.disabled = false;
|
|
228
|
+
answerActionsInFlight.delete(button);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
156
231
|
|
|
157
232
|
let source;
|
|
158
233
|
const terminalAtLoad = document.body.dataset.terminal === "true";
|
|
@@ -589,10 +664,10 @@ export function renderTurnPreviewSnapshot(
|
|
|
589
664
|
: "",
|
|
590
665
|
`<section class="timeline" data-preview-key="timeline"${timelineContent ? "" : " hidden"}>${timelineContent}</section>`,
|
|
591
666
|
state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl, options.deferDetails) : "",
|
|
592
|
-
state.approval ?
|
|
667
|
+
state.approval ? renderApprovalNotice(state.approval, renderText, language) : "",
|
|
593
668
|
state.error ? `<section class="result error-result" data-preview-key="error"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
|
|
594
669
|
state.finalResponse
|
|
595
|
-
?
|
|
670
|
+
? renderFinalAnswer(state.finalResponse, state.turnId, renderText, language)
|
|
596
671
|
: state.assistantText
|
|
597
672
|
? `<section class="result" data-preview-key="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${renderText(state.assistantText)}</div></section>`
|
|
598
673
|
: "",
|
|
@@ -606,6 +681,31 @@ export function renderTurnPreviewSnapshot(
|
|
|
606
681
|
};
|
|
607
682
|
}
|
|
608
683
|
|
|
684
|
+
function renderApprovalNotice(request: ApprovalRequest, renderText: (value: string) => string, language: TurnPreviewLanguage): string {
|
|
685
|
+
const command = request.command?.trim();
|
|
686
|
+
const commandApproval = command && request.kind !== "mode_change";
|
|
687
|
+
const title = commandApproval ? language === "zh" ? "命令执行确认" : "Command approval" : request.title;
|
|
688
|
+
const reason = commandApproval
|
|
689
|
+
? request.reason?.trim() || (request.title.trim() !== command ? request.title : "")
|
|
690
|
+
: request.reason ?? "";
|
|
691
|
+
const description = commandApproval
|
|
692
|
+
? `<p class="approval-reason">${escapeHtml(reason === command ? "" : reason)}</p>`
|
|
693
|
+
: `<div class="markdown">${renderText(reason)}</div>`;
|
|
694
|
+
const details = commandApproval
|
|
695
|
+
? `<details class="approval-details" data-activity-id="approval:${escapeAttribute(request.id)}"><summary>${language === "zh" ? "查看完整命令" : "View full command"}</summary><pre data-scroll-id="approval:${escapeAttribute(request.id)}:command">${escapeHtml(request.command!)}</pre></details>` : "";
|
|
696
|
+
return `<section class="notice" data-preview-key="approval"><h2>${escapeHtml(title)}</h2>${description}${details}<p>${language === "zh" ? "请在飞书任务卡片中确认或拒绝。" : "Approve or reject using the task card in Feishu."}</p></section>`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function renderFinalAnswer(markdown: string, turnId: string, renderText: (value: string) => string, language: TurnPreviewLanguage): string {
|
|
700
|
+
const labels = previewLabels(language);
|
|
701
|
+
const filename = `turn-${turnId.replace(/[^A-Za-z0-9_-]/gu, "_").slice(0, 80) || "answer"}.md`;
|
|
702
|
+
const icon = (paths: string): string => `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths}</svg>`;
|
|
703
|
+
const button = (action: string, label: string, paths: string): string => `<button type="button" class="answer-action" data-answer-action="${action}" title="${escapeAttribute(label)}" aria-label="${escapeAttribute(label)}">${icon(paths)}</button>`;
|
|
704
|
+
const actions = button("download", labels.downloadMarkdown, '<path d="M12 3v12m-5-5 5 5 5-5M5 16v5h14v-5"/>')
|
|
705
|
+
+ button("copy", labels.copyMarkdown, '<rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/>');
|
|
706
|
+
return `<section class="result final-result" data-preview-key="result" data-answer-markdown="${Buffer.from(markdown, "utf8").toString("base64")}" data-answer-filename="${filename}"><div class="result-heading"><h2>${escapeHtml(labels.finalAnswer)}</h2>${actions}<span class="answer-feedback" data-answer-feedback role="status" aria-live="polite"></span></div><div class="markdown">${renderText(markdown)}</div></section>`;
|
|
707
|
+
}
|
|
708
|
+
|
|
609
709
|
function renderPlan(state: TurnViewState, language: TurnPreviewLanguage): string {
|
|
610
710
|
const labels = previewLabels(language);
|
|
611
711
|
const completed = state.plan.filter((step) => step.status === "completed").length;
|
|
@@ -1092,19 +1192,21 @@ function renderMetadata(state: TurnViewState, language: TurnPreviewLanguage): st
|
|
|
1092
1192
|
? state.startedAt + state.durationMs
|
|
1093
1193
|
: undefined);
|
|
1094
1194
|
const elapsed = `<span>${escapeHtml(labels.elapsed)}<strong${isTerminal(state.status) ? "" : ` data-live-elapsed data-started-at="${state.startedAt}"`}>${completedAt === undefined && isTerminal(state.status) ? escapeHtml(labels.unknown) : formatDuration((completedAt ?? Date.now()) - state.startedAt)}</strong></span>`;
|
|
1095
|
-
|
|
1195
|
+
const fields = [
|
|
1096
1196
|
elapsed,
|
|
1197
|
+
state.modelProvider?.trim() ? `<span title="Provider">${escapeHtml(state.modelProvider.trim())}</span>` : "",
|
|
1097
1198
|
state.model?.trim() ? `<span title="${escapeAttribute(labels.model)}">${escapeHtml(state.model.trim())}</span>` : "",
|
|
1098
|
-
state.modelProvider?.trim() ? `<span title="Provider">Provider: ${escapeHtml(state.modelProvider.trim())}</span>` : "",
|
|
1099
1199
|
state.totalTokens === undefined ? "" : state.cachedInputTokens === undefined
|
|
1100
1200
|
? `<span title="${escapeAttribute(labels.turnTokens)}">${formatTokenCount(state.totalTokens)} tokens</span>`
|
|
1101
1201
|
: `<span title="${escapeAttribute(labels.nonCachedTokens)}: ${formatNumber(state.totalTokens)} tokens">${escapeHtml(labels.nonCachedTokens)} ${formatTokenCount(state.totalTokens)} tokens</span>`,
|
|
1102
1202
|
state.totalTokensIncludingCache === undefined ? "" : `<span title="${escapeAttribute(labels.totalTokens)}: ${formatNumber(state.totalTokensIncludingCache)} tokens">${escapeHtml(labels.totalTokens)} ${formatTokenCount(state.totalTokensIncludingCache)} tokens</span>`,
|
|
1103
|
-
state.
|
|
1104
|
-
? `<span title="${escapeAttribute(labels.
|
|
1105
|
-
|
|
1203
|
+
state.latestContextTokens !== undefined && Number.isFinite(state.latestContextTokens) && state.latestContextTokens >= 0
|
|
1204
|
+
? `<span title="${escapeAttribute(labels.contextTokens)}: ${formatNumber(state.latestContextTokens)} tokens">${escapeHtml(labels.contextTokens)} ${formatTokenCount(state.latestContextTokens)} tokens</span>` : "",
|
|
1205
|
+
state.modelCallCount !== undefined && Number.isSafeInteger(state.modelCallCount) && state.modelCallCount >= 0
|
|
1206
|
+
? `<span title="${escapeAttribute(labels.modelCallsDescription)}">${escapeHtml(labels.modelCalls)} ${formatNumber(state.modelCallCount)}${language === "zh" ? " 次" : ""}</span>` : "",
|
|
1106
1207
|
(state.totalToolCount ?? 0) > 0 ? `<span>${formatNumber(state.totalToolCount ?? 0)} ${escapeHtml(labels.tools)}</span>` : "",
|
|
1107
|
-
].filter(Boolean)
|
|
1208
|
+
].filter(Boolean);
|
|
1209
|
+
return fields.join("");
|
|
1108
1210
|
}
|
|
1109
1211
|
|
|
1110
1212
|
function previewTitle(state: TurnViewState): string {
|
|
@@ -1310,6 +1412,17 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
|
|
|
1310
1412
|
.detail-message button { margin-left: 8px; cursor: pointer; }
|
|
1311
1413
|
.tool-image img { display: block; max-width: 100%; max-height: 720px; }
|
|
1312
1414
|
.final-result h2 { color: var(--success); }
|
|
1415
|
+
.approval-reason { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1416
|
+
.approval-reason:empty { display: none; }
|
|
1417
|
+
.approval-details { margin: 8px 0; }
|
|
1418
|
+
.approval-details pre { max-height: 30em; }
|
|
1419
|
+
.result-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-bottom: 8px; }
|
|
1420
|
+
.result-heading h2 { margin: 0 4px 0 0; }
|
|
1421
|
+
.answer-action { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 30px; height: 30px; padding: 0; border: 1px solid transparent; border-radius: 5px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
1422
|
+
.answer-action:hover { color: var(--accent); background: var(--surface); }
|
|
1423
|
+
.answer-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
1424
|
+
.answer-action:disabled { opacity: 0.5; cursor: wait; }
|
|
1425
|
+
.answer-feedback { min-width: 0; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
|
|
1313
1426
|
@media (max-width: 700px) {
|
|
1314
1427
|
.header-inner { padding: 5px 12px; }
|
|
1315
1428
|
.header-top { flex-basis: 100%; }
|
|
@@ -162,6 +162,7 @@ function reduceEvent(state: TurnViewState, event: AgentEvent): TurnViewState {
|
|
|
162
162
|
return {
|
|
163
163
|
...state,
|
|
164
164
|
totalTokens: (state.totalTokens ?? 0) + delta,
|
|
165
|
+
...countModelCall(state, event),
|
|
165
166
|
tokenUsageCumulative: Math.max(previousCumulative ?? 0, cumulativeTokens),
|
|
166
167
|
totalTokensIncludingCache: total.value,
|
|
167
168
|
tokenUsageTotalCumulative: total.cumulative,
|
|
@@ -609,6 +610,46 @@ function bound(value: string): string {
|
|
|
609
610
|
return `${value.slice(0, MAX_TEXT - 1)}…`;
|
|
610
611
|
}
|
|
611
612
|
|
|
613
|
+
function countModelCall(
|
|
614
|
+
state: TurnViewState,
|
|
615
|
+
event: Extract<AgentEvent, { type: "token_usage_updated" }>,
|
|
616
|
+
): Pick<TurnViewState, "modelCallCount" | "modelCallTokenBaseline"> {
|
|
617
|
+
// A pre-existing snapshot without a counter cannot provide a complete turn count.
|
|
618
|
+
if (state.modelCallCount === undefined && state.modelCallTokenBaseline === undefined
|
|
619
|
+
&& state.tokenUsageCumulative !== undefined) return {};
|
|
620
|
+
const nonCached = reportedUsage(event.lastTokens, event.cumulativeTokens);
|
|
621
|
+
const total = reportedUsage(event.lastTotalTokens, event.cumulativeTotalTokens);
|
|
622
|
+
if (!nonCached && !total) return { modelCallTokenBaseline: state.modelCallTokenBaseline ?? {} };
|
|
623
|
+
const previous = state.modelCallTokenBaseline;
|
|
624
|
+
const usage = total ?? nonCached!;
|
|
625
|
+
const staleNonCached = nonCached !== undefined && previous?.nonCached !== undefined
|
|
626
|
+
&& nonCached.cumulative < previous.nonCached;
|
|
627
|
+
const nonCachedGrew = nonCached !== undefined && previous?.nonCached !== undefined
|
|
628
|
+
&& nonCached.cumulative > previous.nonCached;
|
|
629
|
+
const grew = total && previous?.total !== undefined && !previous.totalNeedsRebase
|
|
630
|
+
? total.cumulative > previous.total
|
|
631
|
+
: nonCached && previous?.nonCached !== undefined
|
|
632
|
+
? nonCachedGrew
|
|
633
|
+
: previous?.total === undefined && previous?.nonCached === undefined
|
|
634
|
+
&& usage.last > 0 && usage.cumulative > 0;
|
|
635
|
+
return {
|
|
636
|
+
modelCallCount: (state.modelCallCount ?? 0) + (grew ? 1 : 0),
|
|
637
|
+
modelCallTokenBaseline: {
|
|
638
|
+
nonCached: nonCached ? Math.max(previous?.nonCached ?? 0, nonCached.cumulative) : previous?.nonCached,
|
|
639
|
+
total: total ? Math.max(previous?.total ?? 0, total.cumulative) : previous?.total,
|
|
640
|
+
// After a legacy-only update, rebase totals before using them again; keep
|
|
641
|
+
// their high-water mark so stale reports cannot lower the baseline.
|
|
642
|
+
totalNeedsRebase: total && !staleNonCached ? false : nonCachedGrew || previous?.totalNeedsRebase,
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function reportedUsage(last: number | undefined, cumulative: number | undefined): { last: number; cumulative: number } | undefined {
|
|
648
|
+
if (last === undefined || cumulative === undefined || !Number.isFinite(last) || !Number.isFinite(cumulative)
|
|
649
|
+
|| last < 0 || cumulative < 0) return undefined;
|
|
650
|
+
return { last: normalizeTokenCount(last), cumulative: normalizeTokenCount(cumulative) };
|
|
651
|
+
}
|
|
652
|
+
|
|
612
653
|
function accumulateReportedTokens(
|
|
613
654
|
last: number | undefined,
|
|
614
655
|
cumulative: number | undefined,
|
|
@@ -56,6 +56,10 @@ export interface TurnViewState {
|
|
|
56
56
|
cachedInputTokens?: number;
|
|
57
57
|
tokenUsageTotalCumulative?: number;
|
|
58
58
|
tokenUsageCachedCumulative?: number;
|
|
59
|
+
/** Effective token-usage updates observed for this turn. */
|
|
60
|
+
modelCallCount?: number;
|
|
61
|
+
/** Independent high-water marks; token breakdowns may disappear in partial updates. */
|
|
62
|
+
modelCallTokenBaseline?: { nonCached?: number; total?: number; totalNeedsRebase?: boolean };
|
|
59
63
|
contextCompactionStatus?: "running" | "completed";
|
|
60
64
|
contextCompactionCount?: number;
|
|
61
65
|
contextCompactionId?: string;
|