mocode-ai 1.5.5 → 1.5.6

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.
@@ -25,6 +25,9 @@ export async function runModelTurn(input) {
25
25
  const storedCalibration = ctx.getTokenCalibration(requestBaseURL, requestModel, activeTools);
26
26
  runtimeContextState.correction = storedCalibration.correction;
27
27
  runtimeContextState.calibrationSamples = storedCalibration.samples;
28
+ // 记录本步实际发送的工具集(policy 收窄后),底栏用量条按它估算 schema,
29
+ // 而不是全量 chatTools(含未路由的 MCP/写工具),避免凭空高估。
30
+ runtimeContextState.activeTools = activeTools;
28
31
  let sessionStateText = opts.suppressSessionState ? '' : ctx.buildSessionStateReminder();
29
32
  runtimeContextState.ephemeralText = sessionStateText || undefined;
30
33
  onContextUpdate?.();
@@ -243,6 +246,16 @@ export async function runModelTurn(input) {
243
246
  const updated = ctx.updateTokenCalibration(requestBaseURL, requestModel, activeTools, estimated, result.usage.promptTokens);
244
247
  runtimeContextState.correction = updated.correction;
245
248
  runtimeContextState.calibrationSamples = updated.samples;
249
+ // 钉住底栏用量条:此刻 requestHistory == 真实发出的请求,实测 promptTokens 是它的真实大小。
250
+ // 记录基线裸估算/history 长度/工具集/ephemeral,供底栏对随后增长的消息做精确外推。
251
+ const requestHistoryLen = requestHistory.length;
252
+ runtimeContextState.promptAnchor = {
253
+ measuredPromptTokens: result.usage.promptTokens,
254
+ baseRaw: estimated,
255
+ historyLen: sessionStateText ? requestHistoryLen - 1 : requestHistoryLen,
256
+ tools: activeTools,
257
+ ephemeralText: sessionStateText,
258
+ };
246
259
  }
247
260
  hooks.onChatDone?.();
248
261
  onContextUpdate?.();
@@ -20,6 +20,8 @@ export const sessionCommands = [
20
20
  ctx.contextState.lastUsage = undefined;
21
21
  ctx.contextState.lifecycleStats = undefined;
22
22
  ctx.contextState.ephemeralText = undefined;
23
+ ctx.contextState.activeTools = undefined;
24
+ ctx.contextState.promptAnchor = undefined; // 新会话不得用旧会话的实测锚点外推
23
25
  ctx.state.lastTurnUsage = undefined; // 清空旧轮的 token 累计
24
26
  ctx.state.lastToolGroups = []; // 新会话不得继承旧会话的路由回退
25
27
  ctx.attachments.clear(); // 一并清空待发图片
@@ -639,6 +639,8 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
639
639
  contextState.lastUsage = undefined;
640
640
  contextState.lifecycleStats = undefined;
641
641
  contextState.ephemeralText = undefined;
642
+ contextState.activeTools = undefined;
643
+ contextState.promptAnchor = undefined; // 续接:旧会话的实测锚点不得外推到新 history
642
644
  lastTurnUsage = undefined; // 续接:旧会话的 token 累计已无意义,清空等下轮覆写
643
645
  layout.clearContent();
644
646
  // 锚点行:会话 id / 消息数 / 当前模型,给续接的会话一个定位起点(与 --resume 启动路径同文案)。
@@ -6,7 +6,7 @@ import { t } from '../i18n/index.js';
6
6
  import { config } from '../config/index.js';
7
7
  import { contextState } from '../session/index.js';
8
8
  import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
9
- import { estimateMessagesTokens, estimatePromptTokens, estimateTokens, chatTools } from '../llm/index.js';
9
+ import { chatTools, correctTokenEstimate, estimateMessagesTokens, estimateToolSchemaTokens, estimateTokens, } from '../llm/index.js';
10
10
  import { computePruneStats } from '../context/relevance.js';
11
11
  import { formatArtifactTokenSources } from '../context/artifacts.js';
12
12
  import { getAgentMode } from '../agent/mode.js';
@@ -51,7 +51,9 @@ export function renderContextBar(history) {
51
51
  return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${Math.round(pct * 100)}% ${k(est)}/${k(win)} tokens · ${t('status.messages', { count: history.length })} (${src})${measuredNote}${ui.reset}${artifactLine}${lifecycleLine}${archiveLine}`;
52
52
  }
53
53
  /** 状态行用量条(精简版,进底栏):[bar] pct% k/k。
54
- * 必须**用全 prompt 估算**(消息 + 工具 schema + 尾部 ephemeral 注入),与压缩触发器
54
+ * 空对话(只有 system 提示、还没发任何消息)恒显 0% 0/window:固定 system prompt + 工具
55
+ * schema 是每次请求的基础设施开销,不算用户占用的上下文,避免刚进 REPL 就显示十几 k。
56
+ * 有对话后必须**用全 prompt 估算**(消息 + 工具 schema + 尾部 ephemeral 注入),与压缩触发器
55
57
  * evaluateBudget 的 system+history+toolOld+toolRecent 总账对齐——任何一段漏算都会让
56
58
  * bar 与触发器口径不一致、看着没到 80% 实际已经在压。
57
59
  * 触发器用 `Math.max(rawTotal, total) >= 0.8 * window`,bar 也照搬:校正后和校正前
@@ -63,17 +65,50 @@ export function renderContextBar(history) {
63
65
  * /context 命令仍是 dialog-only(见 renderContextBar):它的设计意图是"我说了多少"而非
64
66
  * "还剩多少空间",两条职责分开。 */
65
67
  export function renderContextBarInline(history) {
66
- const baseRaw = estimatePromptTokens(history, chatTools, 1);
67
- const baseAdj = estimatePromptTokens(history, chatTools, contextState.correction);
68
- const ephemeral = contextState.ephemeralText ? estimateTokens(contextState.ephemeralText) : 0;
69
- // 与触发器同样的「取大」语义:correction<1 时 raw 更大,bar 不会假装很安全。
70
- const est = Math.max(baseRaw, baseAdj) + ephemeral;
71
68
  const win = config.contextWindowTokens;
69
+ let est;
70
+ // 还没有任何真实对话(只有 system 提示)时显示 0:system prompt 与工具 schema 是每次请求
71
+ // 都要发的固定"基础设施开销",不属于用户对话占用的上下文——刚进 REPL 一句话没问就显示
72
+ // 十几 k 会让人误以为已经用掉了上下文。发送第一条消息后才计入。与 /context 的 dialog-only
73
+ // 口径一致;压缩触发器(evaluateBudget)不受影响,它只在真正发请求时按全量评估。
74
+ if (!history.some((m) => m.role !== 'system')) {
75
+ est = 0;
76
+ }
77
+ else {
78
+ // schema 必须按「最近一步实际发送的工具集」估算(ToolPolicy 收窄后);缺省(尚未跑过任何
79
+ // 模型步)才回退到全量 chatTools。旧实现恒用全量,会把未路由的 MCP/写工具 schema 凭空计入,
80
+ // 导致底栏 ~16k 而本轮 API 实测只有 ~5k。
81
+ const activeTools = contextState.activeTools ?? chatTools;
82
+ const ephemeralRaw = contextState.ephemeralText ? estimateTokens(contextState.ephemeralText) : 0;
83
+ const fullRaw = estimateMessagesTokens(history) + estimateToolSchemaTokens(activeTools) + ephemeralRaw;
84
+ const anchor = contextState.promptAnchor;
85
+ // 锚点有效:锚定时 history 是当前 history 的严格前缀(同一长度或更长),工具集/ephemeral
86
+ // 未变,且此后未发生压缩(裸估算不小于锚点基线)。用「实测值 + 新增消息的裸估算」外推,
87
+ // 单步轮 history 未变时结果 == API 实测 promptTokens,与轮末行的 ↑ 数字精确一致。
88
+ if (anchor &&
89
+ history.length >= anchor.historyLen &&
90
+ anchor.tools === activeTools &&
91
+ anchor.ephemeralText === (contextState.ephemeralText ?? '') &&
92
+ // 容差 4:锚点把 ephemeral 当独立 system 消息(3 priming + 结构开销),bar 把它单独相加,
93
+ // 同一快照下 fullRaw 可比 baseRaw 小 3;压缩后 fullRaw 会骤降数千 token,不会被该容差掩盖。
94
+ fullRaw >= anchor.baseRaw - 4) {
95
+ const anchorHistory = history.slice(0, anchor.historyLen);
96
+ const grownRaw = estimateMessagesTokens(history) - estimateMessagesTokens(anchorHistory);
97
+ est = anchor.measuredPromptTokens + Math.max(0, grownRaw);
98
+ }
99
+ else {
100
+ // 无有效锚点(/clear 后、压缩后、工具集或 ephemeral 刚变化):走保守估算,
101
+ // 与触发器同样的「取大」语义——correction<1 时 raw 更大,bar 不会假装很安全。
102
+ const baseAdj = correctTokenEstimate(fullRaw, contextState.correction);
103
+ est = Math.max(fullRaw, baseAdj);
104
+ }
105
+ }
72
106
  const pct = Math.min(1, est / win);
73
107
  const W = 10;
74
108
  const filled = Math.round(pct * W);
75
109
  const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
76
- const k = (n) => `${Math.round(n / 1000)}k`;
110
+ // 与轮末 token 行 formatTurnTokens 同一格式:<10k 保留一位小数,锚点生效时两处逐字一致(5.2k)。
111
+ const k = (n) => (n < 1000 ? `${Math.round(n)}` : `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`);
77
112
  const pctCol = pct >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio ? ui.yellow : ui.accent;
78
113
  return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${pctCol}${Math.round(pct * 100)}%${ui.reset} ${ui.dim}${k(est)}/${k(win)}${ui.reset}`;
79
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.5.5",
3
+ "version": "1.5.6",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 25 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {