@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +22 -4
  3. package/README.zh.md +22 -4
  4. package/dist/acp/AcpSessionManager.js +0 -14
  5. package/dist/acp/AcpSessionManager.js.map +1 -1
  6. package/dist/acp/acpTypes.js.map +1 -1
  7. package/dist/codex/AppServerPermissions.js +34 -0
  8. package/dist/codex/AppServerPermissions.js.map +1 -0
  9. package/dist/codex/CodexLocalActivityDetector.js +37 -5
  10. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  11. package/dist/codex/CodexRuntime.js +37 -13
  12. package/dist/codex/CodexRuntime.js.map +1 -1
  13. package/dist/codex/ThreadWriterProcess.js +19 -5
  14. package/dist/codex/ThreadWriterProcess.js.map +1 -1
  15. package/dist/commands/CommandRouter.js +0 -3
  16. package/dist/commands/CommandRouter.js.map +1 -1
  17. package/dist/feishu/CardRenderer.js +80 -64
  18. package/dist/feishu/CardRenderer.js.map +1 -1
  19. package/dist/local-files/CsvPreview.js +148 -0
  20. package/dist/local-files/CsvPreview.js.map +1 -0
  21. package/dist/local-files/LocalFileViewerServer.js +53 -10
  22. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  23. package/dist/local-files/TurnPreviewPage.js +125 -14
  24. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  25. package/dist/presentation/TurnStateReducer.js +39 -0
  26. package/dist/presentation/TurnStateReducer.js.map +1 -1
  27. package/dist/proxy/ProxySessionController.js +158 -50
  28. package/dist/proxy/ProxySessionController.js.map +1 -1
  29. package/docs/technical-reference.md +5 -3
  30. package/docs/technical-reference.zh.md +4 -2
  31. package/npm-shrinkwrap.json +2 -2
  32. package/package.json +1 -1
  33. package/skills/agent-bot/SKILL.md +10 -0
  34. package/src/acp/AcpSessionManager.ts +0 -17
  35. package/src/acp/acpTypes.ts +0 -1
  36. package/src/codex/AppServerPermissions.ts +39 -0
  37. package/src/codex/CodexLocalActivityDetector.ts +34 -4
  38. package/src/codex/CodexRuntime.ts +44 -17
  39. package/src/codex/ThreadWriterProcess.ts +17 -9
  40. package/src/commands/CommandRouter.ts +0 -3
  41. package/src/feishu/CardRenderer.ts +78 -64
  42. package/src/local-files/CsvPreview.ts +132 -0
  43. package/src/local-files/LocalFileViewerServer.ts +54 -11
  44. package/src/local-files/TurnPreviewPage.ts +128 -15
  45. package/src/presentation/TurnStateReducer.ts +41 -0
  46. package/src/presentation/turnViewTypes.ts +4 -0
  47. package/src/proxy/ProxySessionController.ts +142 -51
  48. 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: "Running tool",
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 ? `<section class="notice" data-preview-key="approval"><h2>${escapeHtml(state.approval.title)}</h2><div class="markdown">${renderText(state.approval.reason ?? "")}</div><p>${escapeHtml(language === "zh" ? "请在飞书任务卡片中确认或拒绝。" : "Approve or reject using the task card in Feishu.")}</p></section>` : "",
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
- ? `<section class="result final-result" data-preview-key="result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderText(state.finalResponse)}</div></section>`
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
- return [
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.contextCompactionAfterTokens === undefined ? "" : state.contextCompactionBeforeTokens === undefined
1104
- ? `<span title="${escapeAttribute(labels.compactionAfterTokens)}">${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`
1105
- : `<span title="${escapeAttribute(labels.compactionTokens)}">${formatTokenCount(state.contextCompactionBeforeTokens)} → ${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`,
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).join("");
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;