mocode-ai 1.1.7 → 1.1.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.
Files changed (51) hide show
  1. package/README.md +23 -37
  2. package/README.zh-CN.md +46 -38
  3. package/dist/agent/core.js +100 -446
  4. package/dist/agent/index.js +2 -21
  5. package/dist/agent/spawn.js +3 -5
  6. package/dist/agent/work-discipline.js +16 -70
  7. package/dist/config/index.js +6 -7
  8. package/dist/context/age-aware.js +18 -48
  9. package/dist/context/artifacts.js +19 -17
  10. package/dist/context/budget.js +27 -28
  11. package/dist/context/classifier.js +0 -1
  12. package/dist/context/encoders/index.js +4 -11
  13. package/dist/context/index.js +4 -7
  14. package/dist/context/lifecycle.js +115 -483
  15. package/dist/context/pipeline.js +8 -15
  16. package/dist/context/relevance.js +77 -55
  17. package/dist/host/stdio.js +0 -6
  18. package/dist/i18n/index.js +0 -6
  19. package/dist/index.js +11 -1
  20. package/dist/llm/index.js +72 -3
  21. package/dist/mcp/index.js +0 -1
  22. package/dist/repl/index.js +21 -15
  23. package/dist/runtime/browser-manager.js +299 -0
  24. package/dist/runtime/dev-server-manager.js +354 -0
  25. package/dist/runtime/shutdown.js +26 -0
  26. package/dist/session/compact.js +86 -102
  27. package/dist/session/index.js +0 -1
  28. package/dist/session/scheduler.js +88 -92
  29. package/dist/session/trace-metrics.js +5 -92
  30. package/dist/session/trace.js +1 -10
  31. package/dist/tools/builtins/browser.js +199 -0
  32. package/dist/tools/builtins/dev-server.js +99 -0
  33. package/dist/tools/builtins/index.js +28 -19
  34. package/dist/tools/builtins/screenshot.js +173 -0
  35. package/dist/tools/builtins/view-image.js +49 -0
  36. package/dist/tools/constants.js +3 -0
  37. package/dist/tools/registry.js +5 -29
  38. package/dist/ui/layout.js +28 -5
  39. package/dist/ui/render.js +11 -0
  40. package/package.json +2 -2
  41. package/dist/agent/middleware/checklist.js +0 -59
  42. package/dist/session/drop.d.ts +0 -19
  43. package/dist/session/drop.js +0 -93
  44. package/dist/tools/builtins/drop-context.d.ts +0 -18
  45. package/dist/tools/builtins/drop-context.js +0 -68
  46. package/dist/verification/diagnostics.js +0 -108
  47. package/dist/verification/fingerprint.js +0 -54
  48. package/dist/verification/index.js +0 -333
  49. package/dist/verification/postconditions.js +0 -98
  50. package/dist/verification/targeted-tests.js +0 -96
  51. package/dist/verification/types.js +0 -1
@@ -187,6 +187,39 @@ function flattenGroups(groups) {
187
187
  }
188
188
  return out;
189
189
  }
190
+ /** 微压缩单个 group(原地):① tool 结果 content ② assistant tool_calls.arguments(provenance stub)
191
+ * ③ assistant 正文。只裁模型/工具产物,不动 user 原话;保 tool_call_id 配对。返回是否有改动。 */
192
+ function microcompactGroup(g) {
193
+ let done = false;
194
+ for (const t of g.tools) {
195
+ const c = t.content;
196
+ if (typeof c === 'string' && c.length > MAX_OLD_TOOL_STUB) {
197
+ t.content = truncateMid(c, MAX_OLD_TOOL_STUB);
198
+ done = true;
199
+ }
200
+ }
201
+ const as = g.assistant;
202
+ if (as && as.role === 'assistant') {
203
+ if (typeof as.content === 'string' && as.content.length > MAX_OLD_TOOL_STUB) {
204
+ as.content = truncateMid(as.content, MAX_OLD_TOOL_STUB);
205
+ done = true;
206
+ }
207
+ // tool_calls 参数:整体超长才进,provenance stub 大字段(保 path + JSON 合法)。
208
+ if (Array.isArray(as.tool_calls)) {
209
+ for (const tc of as.tool_calls) {
210
+ const args = tc?.function?.arguments;
211
+ if (typeof args !== 'string' || args.length <= MAX_OLD_TOOL_STUB)
212
+ continue;
213
+ const stubbed = stubToolCallArguments(args);
214
+ if (stubbed !== args) {
215
+ tc.function.arguments = stubbed;
216
+ done = true;
217
+ }
218
+ }
219
+ }
220
+ }
221
+ return done;
222
+ }
190
223
  // ── 默认摘要器:复用 chat(),空 handlers 不打印 ──────────────────────────
191
224
  async function defaultSummarize(older, focus) {
192
225
  // 摘要前剥离多模态 user 消息里的图片(base64 会撑爆摘要 prompt;image 对摘要无信息量)。
@@ -256,7 +289,14 @@ export async function compactHistory(history, opts) {
256
289
  kept.unshift(g);
257
290
  keptTokens = nextTokens;
258
291
  }
259
- const oldGroups = groups.slice(0, groups.length - kept.length);
292
+ let oldGroups = groups.slice(0, groups.length - kept.length);
293
+ // force(硬闸/手动强压):保护区不豁免——常规切分无旧区时只保最后一组,
294
+ // 其余全部进可压区(首轮/当前轮也一样)。仍按 group 边界切,不破坏 tool_call 配对。
295
+ if (oldGroups.length === 0 && opts.force && groups.length >= 2) {
296
+ kept.length = 0;
297
+ kept.push(groups[groups.length - 1]);
298
+ oldGroups = groups.slice(0, groups.length - 1);
299
+ }
260
300
  const noop = {
261
301
  compacted: false,
262
302
  summarized: false,
@@ -269,65 +309,27 @@ export async function compactHistory(history, opts) {
269
309
  if (oldGroups.length === 0) {
270
310
  // 没有旧区可压缩
271
311
  const protectedRatio = history.length > 0 ? kept.length / history.length : 0;
272
- if (opts.force && history.length > 2) {
273
- // 真·强制压:把降 keepBudget 当 old group 强行创建一组可压区
274
- // 取最早的 user/assistant/tool 当 old,后一段当 kept
275
- const mid = Math.max(1, Math.floor(history.length / 2));
276
- const older = history.slice(1, mid);
277
- const keptAfter = history.slice(mid);
278
- // 走仅微压缩(LLM 可能不可用,不强行 summarize)
279
- let microcompactDone2 = false;
280
- for (const m of older) {
281
- const c = m.content;
282
- if (typeof c === 'string' && c.length > MAX_OLD_TOOL_STUB) {
283
- m.content = truncateMid(c, MAX_OLD_TOOL_STUB);
284
- microcompactDone2 = true;
285
- }
286
- const as = m;
287
- if (as.role === 'assistant') {
288
- if (typeof as.content === 'string' && as.content.length > MAX_OLD_TOOL_STUB) {
289
- as.content = truncateMid(as.content, MAX_OLD_TOOL_STUB);
290
- microcompactDone2 = true;
291
- }
292
- if (Array.isArray(as.tool_calls)) {
293
- for (const tc of as.tool_calls) {
294
- const args = tc?.function?.arguments;
295
- if (typeof args !== 'string' || args.length <= MAX_OLD_TOOL_STUB)
296
- continue;
297
- const stubbed = stubToolCallArguments(args);
298
- if (stubbed !== args) {
299
- tc.function.arguments = stubbed;
300
- microcompactDone2 = true;
301
- }
302
- }
303
- }
304
- }
312
+ if (opts.force && groups.length === 1) {
313
+ // force 但只剩单组(如首轮的单个 assistant+tools 组):原地微压缩、不动结构。
314
+ // 纯 user 原话组无可压项(不能毁掉用户请求),落到下方 noop 提示。
315
+ const single = groups[0];
316
+ const userOnly = single.tools.length === 0
317
+ && single.assistant?.role === 'user';
318
+ if (!userOnly && microcompactGroup(single)) {
319
+ const estimateAfter = estimatePromptTokens(history, activeTools, state.correction);
320
+ state.lastEstimate = estimateAfter;
321
+ state.lastUsage = undefined;
322
+ layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}强制微压缩(单组)${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
323
+ return {
324
+ compacted: true,
325
+ summarized: false,
326
+ estimateBefore,
327
+ estimateAfter,
328
+ reason: 'microcompact',
329
+ protectedRatio,
330
+ oldGroupCount: 0,
331
+ };
305
332
  }
306
- const refs = collectArtifactRefs(older);
307
- const summaryMsg = {
308
- role: 'system',
309
- content: older.length > 0
310
- ? `# 会话摘要(force)\n被跳过的早期对话 ${older.length} 条已微压缩(token 数减少)。${refs.length > 0 ? `\n[artifact refs: ${refs.join(', ')}]` : ''}`
311
- : `# 会话摘要(force)\n无内容。`,
312
- };
313
- const rebuilt = [history[0], summaryMsg, ...keptAfter];
314
- history.length = 0;
315
- history.push(...rebuilt);
316
- pruneAfterCompaction(history);
317
- const estimateAfter = estimatePromptTokens(history, activeTools, state.correction);
318
- state.lastEstimate = estimateAfter;
319
- state.lastUsage = undefined;
320
- layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}强制压缩(focus on early history)${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
321
- return {
322
- compacted: true,
323
- summarized: false,
324
- historyRebuilt: true,
325
- estimateBefore,
326
- estimateAfter,
327
- reason: microcompactDone2 ? 'microcompact' : 'summarize',
328
- protectedRatio,
329
- oldGroupCount: 0,
330
- };
331
333
  }
332
334
  // 细分 noop 类型,供 repl 文案
333
335
  const isEmpty = history.length <= 1; // 只有 system 提示
@@ -350,35 +352,8 @@ export async function compactHistory(history, opts) {
350
352
  // ③ 旧 assistant 正文 content(模型长解释,回看价值低)。
351
353
  let microcompactDone = false;
352
354
  for (const g of oldGroups) {
353
- for (const t of g.tools) {
354
- const c = t.content;
355
- if (typeof c === 'string' && c.length > MAX_OLD_TOOL_STUB) {
356
- t.content = truncateMid(c, MAX_OLD_TOOL_STUB);
357
- microcompactDone = true;
358
- }
359
- }
360
- const as = g.assistant;
361
- if (as && as.role === 'assistant') {
362
- // ③ 旧正文 content
363
- if (typeof as.content === 'string' && as.content.length > MAX_OLD_TOOL_STUB) {
364
- as.content = truncateMid(as.content, MAX_OLD_TOOL_STUB);
365
- microcompactDone = true;
366
- }
367
- // ② tool_calls 参数:整体超长才进,provenance stub 大字段(保 path + JSON 合法)。
368
- // stubToolCallArguments:大字符串字段 → "<N 字符,已省略>";path 永不省(/rollback 依赖)。
369
- if (Array.isArray(as.tool_calls)) {
370
- for (const tc of as.tool_calls) {
371
- const args = tc?.function?.arguments;
372
- if (typeof args !== 'string' || args.length <= MAX_OLD_TOOL_STUB)
373
- continue;
374
- const stubbed = stubToolCallArguments(args);
375
- if (stubbed !== args) {
376
- tc.function.arguments = stubbed;
377
- microcompactDone = true;
378
- }
379
- }
380
- }
381
- }
355
+ if (microcompactGroup(g))
356
+ microcompactDone = true;
382
357
  }
383
358
  // 第二层:摘要——把旧区(微压缩后)压成一条 system 摘要
384
359
  const older = flattenGroups(oldGroups);
@@ -450,14 +425,14 @@ export async function compactHistory(history, opts) {
450
425
  * 自动压缩门槛:agent 每步调 chat() 前调用。
451
426
  * 用全量启发式估算(始终可用、安全侧、无 stale-usage 问题);超阈则压缩。
452
427
  *
453
- * 升级到 Budget Scheduler:可传 `report: BudgetReport`。传了就**按 ROI 调度**:
454
- * - 只有当 `report.layers.history.overBudget` 或 `report.totalOver` 时才压;
455
- * - 冷工具超(Cold Tool ROI 最低)→ 不压 history,留给调度器的 cold tools 路径处理。
456
- * 这样 cold tools 路径(L1 中截 / L2 relevance / L3 age stub)能先动,history
457
- * 摘要(最贵)只在 cold tools 解不开时才触发。
428
+ * The shared 80% pressure threshold is the only automatic trigger. Raw estimates
429
+ * participate so correction<1 cannot hide an oversized provider request.
430
+ *
431
+ * Budget Scheduler 模式在同一次 80% pressure 事件中先完成已启用的
432
+ * superseded / stale artifact / logs-search 清理,然后无条件继续压缩历史。
458
433
  *
459
- * 不传 report 时退化为原行为:仅看总占用是否超 `compactThreshold * window`——
460
- * 兼容老调用方(子 agent / 测试直接调时),零行为变化。
434
+ * 不传 report 时使用同一个共享阈值,供子 agent 和关闭 scheduler 时 fallback;
435
+ * 不再维护独立的自动压缩触发线。
461
436
  *
462
437
  * manual 选项(repl /compact 用):true 时旁路 autoCompact 开关与 ROI 阈,
463
438
  * 强制走 compactHistory(manual/force 参数透传)。返 CompactResult 给 caller 文案展示。
@@ -467,28 +442,37 @@ export async function maybeCompact(history, report, manualOpts, state = contextS
467
442
  const est = estimatePromptTokens(history, activeTools, state.correction);
468
443
  state.lastEstimate = est;
469
444
  const isManual = manualOpts?.manual === true;
445
+ // Hard guard: the scheduler has already attempted pressure cleanup; if raw
446
+ // content remains near the provider limit, compact even when correction < 1.
447
+ let hardCap = typeof report?.rawTotal === 'number'
448
+ && typeof report?.window === 'number'
449
+ && report.rawTotal >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio * report.window;
470
450
  // 手动路径:旁路 autoCompact / report / 总阈三重门
471
451
  if (!isManual) {
472
- if (!config.autoCompact)
452
+ if (!hardCap && !config.autoCompact)
473
453
  return;
474
454
  if (report) {
475
- // 调度模式:只压 history 层超或兜底总超
476
- const needsCompact = report.layers.history.overBudget || report.totalOver;
455
+ // Scheduler mode: the shared pressure report is the sole automatic trigger.
456
+ const needsCompact = hardCap || report.totalOver;
477
457
  if (!needsCompact)
478
458
  return;
479
459
  }
480
460
  else {
481
- // 老路径:总占用超阈才压
482
- if (est < config.compactThreshold * config.contextWindowTokens)
461
+ // Fallback mode uses the same threshold; there is no second compact gate.
462
+ const raw = estimatePromptTokens(history, activeTools, 1);
463
+ hardCap = raw >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio * config.contextWindowTokens;
464
+ const pressureLine = DEFAULT_BUDGET_POLICY.pressureTriggerRatio * config.contextWindowTokens;
465
+ if (!hardCap && est < pressureLine)
483
466
  return;
484
467
  }
485
468
  }
486
469
  const r = await compactHistory(history, {
487
470
  window: config.contextWindowTokens,
488
- threshold: config.compactThreshold,
471
+ threshold: DEFAULT_BUDGET_POLICY.pressureTriggerRatio,
489
472
  focus: manualOpts?.focus,
490
473
  manual: isManual,
491
- force: manualOpts?.force,
474
+ // 硬闸触发时 force 直通保护区:首轮/当前轮不豁免。
475
+ force: manualOpts?.force === true || hardCap,
492
476
  tools: activeTools,
493
477
  contextState: state,
494
478
  });
@@ -10,7 +10,6 @@ export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, con
10
10
  // 只执行可落地的 warn / compact_history;开关关闭时退化为 maybeCompact。
11
11
  // repl /compact 命令调 manualCompact(history, focus?):与自动路径共享决策,focus 透传摘要 prompt。
12
12
  export { runScheduler, manualCompact, createBudgetScheduler, } from './scheduler.js';
13
- export { dropContextFromHistory, formatDropResult, } from './drop.js';
14
13
  export { newSessionId, saveSession, loadSession, listSessions, sessionDir, } from './persist.js';
15
14
  export { appendCurrentSessionTrace, appendCurrentSessionTraceEvent, appendCurrentSessionRuntimeEvent, createTraceEvent, } from './trace.js';
16
15
  export { reduceTraceMetrics, readTraceEvents, readTraceMetrics } from './trace-metrics.js';
@@ -1,159 +1,155 @@
1
- // Context Budget Scheduler(执行层):评估五区预算并执行可落地的动作。
2
- //
3
- // agent/core.ts (步前) repl /compact 命令(手动)
4
- // ↓ runScheduler(history, step) ↓ manualCompact(history, focus?)
5
- // session/scheduler.ts(本文件)
6
- // ├─ evaluateBudget(history, window, step) → BudgetReport
7
- // ├─ scheduleActions(report) → warn | compact_history
8
- // └─ 执行 actions:
9
- // - warn: 仅写日志
10
- // - compact_history: 调 maybeCompact(history, report)
11
- //
12
- // push-time cap / relevance / lifecycle / age-aware sweep 在进入 scheduler 前独立完成,
13
- // scheduler 不再生成无法执行的 Cold/Hot tool action。每次决策写入 actionLog,供 /context 调试。
14
- // manualCompact 与自动路径共享 scheduleActions,但用户显式触发时强制追加 compact_history。
15
- //
16
- // 开关:
17
- // - config.contextBudget !== false(默认 true):agent 调 runScheduler
18
- // - 关时 agent 仍走原 maybeCompact(history)无 report 路径,零行为变化
19
- // - 手动 /compact 走 manualCompact;关时退化直接调 compactHistory(history, { focus })
20
- import { evaluateBudget, scheduleActions, formatReport, } from '../context/budget.js';
1
+ // Context Budget Scheduler: normal operation is metadata-only. History content is
2
+ // rewritten only after real occupancy reaches the shared pressure threshold.
3
+ import { evaluateBudget, scheduleActions, formatReport, DEFAULT_BUDGET_POLICY, } from '../context/budget.js';
21
4
  import { chatTools } from '../llm/index.js';
22
5
  import { config } from '../config/index.js';
23
6
  import { maybeCompact, contextState } from './compact.js';
24
7
  import * as layout from '../ui/layout.js';
25
8
  import { ui } from '../ui/theme.js';
26
9
  import { pruneStaleArtifacts, refreshArtifactFreshness } from '../context/artifacts.js';
27
- /** 创建 runAgentCore 闭包持有的 scheduler(每次 agent 启动一个新实例)。 */
10
+ import { pruneSuperseded } from '../context/relevance.js';
11
+ import { createAgeAwareEncodingState } from '../context/age-aware.js';
12
+ function atPressure(report) {
13
+ return Math.max(report.rawTotal, report.total)
14
+ >= report.window * DEFAULT_BUDGET_POLICY.pressureTriggerRatio;
15
+ }
16
+ function emptyPressure(report) {
17
+ return {
18
+ triggered: false,
19
+ before: report.total,
20
+ after: report.total,
21
+ superseded: 0,
22
+ staleArtifacts: 0,
23
+ encodedLogsAndSearches: 0,
24
+ };
25
+ }
26
+ /** One scheduler instance is owned by one agent run. */
28
27
  export function createBudgetScheduler(state = contextState) {
29
- const obs = {
28
+ const evaluate = (history, step, activeTools) => evaluateBudget(history, config.contextWindowTokens, step, state.correction, activeTools);
29
+ const scheduler = {
30
30
  lastRunLog: null,
31
31
  async runStep(history, step, activeTools = chatTools) {
32
- // Re-check file-backed hashes first, then discard stale/rebuildable facts before budgeting.
32
+ // External file changes and mutations only update artifact metadata here.
33
33
  refreshArtifactFreshness(state, history);
34
- pruneStaleArtifacts(state, history);
35
- const report = evaluateBudget(history, config.contextWindowTokens, step, state.correction, activeTools);
34
+ const report = evaluate(history, step, activeTools);
35
+ const pressure = emptyPressure(report);
36
+ pressure.triggered = atPressure(report);
37
+ if (pressure.triggered) {
38
+ // A single 80% pressure event owns every history rewrite. Run all enabled
39
+ // low-cost cleanup first, then always compact; do not introduce per-stage
40
+ // thresholds or stop early when one stage happens to cross below 80%.
41
+ if (config.contextRelprune) {
42
+ pressure.superseded = pruneSuperseded(history, report.hotBoundary);
43
+ }
44
+ pressure.staleArtifacts = pruneStaleArtifacts(state, history, report.hotBoundary);
45
+ if (config.contextOptimize) {
46
+ const ageAware = createAgeAwareEncodingState(history);
47
+ pressure.encodedLogsAndSearches = ageAware.sweepPressure(history, report.hotBoundary);
48
+ }
49
+ pressure.after = evaluate(history, step, activeTools).total;
50
+ }
51
+ // Use the trigger report intentionally: cleanup may reduce the current estimate,
52
+ // but crossing 80% commits this step to compacting for maximum token savings.
36
53
  const actions = scheduleActions(report);
37
54
  let compactHistoryCalled = false;
38
55
  let historyRebuilt = false;
39
- for (const a of actions) {
40
- if (a.kind === 'warn') {
41
- // system 超:写一行提示(配置漂移应由用户处理,不是调度器压)
42
- layout.contentWrite(` ${ui.accent}●${ui.reset} ${ui.yellow}调度器警告 [${a.layer}] ${a.reason}${ui.reset}\n`);
43
- }
44
- else if (a.kind === 'compact_history') {
45
- // 路由到 maybeCompact;把结构重建信号传回 core,使 lifecycle 按新 index 恢复。
46
- const result = await maybeCompact(history, report, undefined, state, activeTools);
47
- compactHistoryCalled = true;
48
- historyRebuilt ||= result?.historyRebuilt === true;
56
+ for (const action of actions) {
57
+ if (action.kind === 'warn') {
58
+ layout.contentWrite(` ${ui.accent}●${ui.reset} ${ui.yellow}调度器警告 [${action.layer}] ${action.reason}${ui.reset}\n`);
59
+ continue;
49
60
  }
61
+ const result = await maybeCompact(history, report, undefined, state, activeTools);
62
+ compactHistoryCalled = true;
63
+ historyRebuilt ||= result?.historyRebuilt === true;
50
64
  }
51
65
  const log = {
52
66
  step,
53
67
  report,
54
68
  actions,
69
+ pressure,
55
70
  compactHistoryCalled,
56
71
  ts: Date.now(),
57
72
  };
58
- obs.lastRunLog = log;
59
- // 暴露给 /context 共享读(repl / context 命令)
73
+ scheduler.lastRunLog = log;
60
74
  state.schedulerLog = log;
61
75
  return historyRebuilt;
62
76
  },
63
77
  };
64
- return obs;
78
+ return scheduler;
65
79
  }
66
- /** 便捷:agent/core.ts 不需要每次 createBudgetScheduler,直接 runScheduler(history, step)。 */
67
80
  export async function runScheduler(history, step, state = contextState, activeTools = chatTools) {
68
- const s = createBudgetScheduler(state);
69
- return s.runStep(history, step, activeTools);
81
+ return createBudgetScheduler(state).runStep(history, step, activeTools);
70
82
  }
71
- /** 手动 /compact 入口(repl):与自动路径共享预算评估和可执行 action,但强制执行 history 摘要。
72
- * 即便 layers.history.overBudget=false 或 totalOver=false,manual 仍追加 compact_history,
73
- * 并把 focus 透传给 LLM 摘要 prompt。
74
- *
75
- * 关系:runScheduler 是「自动触发」,manualCompact 是「用户显式触发」,二者共享 scheduleActions。
76
- *
77
- * force=true:即便 oldGroups 空(history 全在保护区)也强行把早期消息降级压一次。
78
- * 适合"history 太长,自动阈值从未触发,但用户想强制压"的场景。
79
- *
80
- * 退化:config.contextBudget === false 时直接调 compactHistory(history, { focus }),与改造前等价。
81
- * 返回 SchedulerRunLog + compactDetail 字段,供 repl 文案展示"为什么没压"。 */
83
+ /** User-requested compaction bypasses automatic pressure gating. */
82
84
  export async function manualCompact(history, focus, opts) {
83
85
  if (config.contextBudget === false) {
84
- // 退化:不经调度器,直压 history(等价于改造前,但带 manual/force 透传)
85
- const r = await import('./compact.js').then(({ compactHistory }) => compactHistory(history, {
86
+ const result = await import('./compact.js').then(({ compactHistory }) => compactHistory(history, {
86
87
  window: config.contextWindowTokens,
87
- threshold: config.compactThreshold,
88
+ threshold: DEFAULT_BUDGET_POLICY.pressureTriggerRatio,
88
89
  focus,
89
90
  manual: true,
90
91
  force: opts?.force,
91
92
  }));
92
- return {
93
+ const report = evaluateBudget(history, config.contextWindowTokens, -1, contextState.correction);
94
+ const log = {
93
95
  step: -1,
94
- report: evaluateBudget(history, config.contextWindowTokens, -1, contextState.correction),
96
+ report,
95
97
  actions: [{ kind: 'compact_history', focus }],
98
+ pressure: emptyPressure(report),
96
99
  compactHistoryCalled: true,
97
100
  ts: Date.now(),
98
101
  compactDetail: {
99
- reason: r.reason,
100
- estimateBefore: r.estimateBefore,
101
- estimateAfter: r.estimateAfter,
102
- protectedRatio: r.protectedRatio,
103
- oldGroupCount: r.oldGroupCount,
102
+ reason: result.reason,
103
+ estimateBefore: result.estimateBefore,
104
+ estimateAfter: result.estimateAfter,
105
+ protectedRatio: result.protectedRatio,
106
+ oldGroupCount: result.oldGroupCount,
104
107
  focus,
105
108
  },
106
109
  };
110
+ contextState.schedulerLog = log;
111
+ return log;
107
112
  }
108
113
  const report = evaluateBudget(history, config.contextWindowTokens, -1, contextState.correction);
109
114
  let actions = scheduleActions(report);
110
- // 用户显式说「要压」:即使 report 不含 history 触发,仍追加 compact_history
111
- const hasCompact = actions.some(a => a.kind === 'compact_history');
112
- if (!hasCompact) {
115
+ if (!actions.some((action) => action.kind === 'compact_history')) {
113
116
  actions = [...actions, { kind: 'compact_history', focus }];
114
117
  }
115
118
  else if (focus) {
116
- // 已有 compact_history(action 由 scheduleActions 产,无 focus)— 注入 focus
117
- actions = actions.map(a => a.kind === 'compact_history' ? { ...a, focus } : a);
119
+ actions = actions.map((action) => action.kind === 'compact_history' ? { ...action, focus } : action);
118
120
  }
119
- const s = createBudgetScheduler();
120
- // 直接驱动 runStep 的执行逻辑(action list 我们已自己生成)
121
121
  let compactHistoryCalled = false;
122
122
  let compactDetail;
123
- for (const a of actions) {
124
- if (a.kind === 'warn') {
125
- // 复用现有 contentWrite 路径(经 scheduler.runStep);这里略,只记录到 log
126
- }
127
- else if (a.kind === 'compact_history') {
128
- const r = await maybeCompact(history, report, {
129
- manual: true,
130
- force: opts?.force,
131
- focus: a.focus,
132
- });
133
- compactHistoryCalled = true;
134
- if (r) {
135
- compactDetail = {
136
- reason: r.reason,
137
- estimateBefore: r.estimateBefore,
138
- estimateAfter: r.estimateAfter,
139
- protectedRatio: r.protectedRatio,
140
- oldGroupCount: r.oldGroupCount,
141
- focus: a.focus,
142
- };
143
- }
123
+ for (const action of actions) {
124
+ if (action.kind !== 'compact_history')
125
+ continue;
126
+ const result = await maybeCompact(history, report, {
127
+ manual: true,
128
+ force: opts?.force,
129
+ focus: action.focus,
130
+ });
131
+ compactHistoryCalled = true;
132
+ if (result) {
133
+ compactDetail = {
134
+ reason: result.reason,
135
+ estimateBefore: result.estimateBefore,
136
+ estimateAfter: result.estimateAfter,
137
+ protectedRatio: result.protectedRatio,
138
+ oldGroupCount: result.oldGroupCount,
139
+ focus: action.focus,
140
+ };
144
141
  }
145
142
  }
146
143
  const log = {
147
144
  step: -1,
148
145
  report,
149
146
  actions,
147
+ pressure: emptyPressure(report),
150
148
  compactHistoryCalled,
151
149
  ts: Date.now(),
152
150
  compactDetail,
153
151
  };
154
- s.lastRunLog = log;
155
152
  contextState.schedulerLog = log;
156
153
  return log;
157
154
  }
158
- // ── 调试导出:用于 scripts/check-budget.ts 把报告打到 stdout 调试 ────────
159
155
  export { evaluateBudget, scheduleActions, formatReport };
@@ -1,99 +1,16 @@
1
1
  import { readFileSync } from 'node:fs';
2
- /** 把 history 中所有 user/tool/assistant 消息的文本/参数汇总到一个大字符串,便于
3
- * 用单一正则扫描多个 marker。比逐条消息 join 更稳:LLM 流式 contentPart 数组
4
- * 与多模态混合时,joinStringField 内部把每个 part 展平成字符串。 */
5
- function joinHistoryForMarkerScan(history) {
6
- if (!history)
7
- return '';
8
- const parts = [];
9
- for (const message of history) {
10
- if (typeof message.content === 'string') {
11
- parts.push(message.content);
12
- }
13
- else if (Array.isArray(message.content)) {
14
- for (const part of message.content) {
15
- if (typeof part === 'string')
16
- parts.push(part);
17
- else if (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') {
18
- parts.push(part.text);
19
- }
20
- }
21
- }
22
- if ('tool_call_id' in message && typeof message.tool_call_id === 'string') {
23
- parts.push(message.tool_call_id);
24
- }
25
- }
26
- return parts.join('\n');
27
- }
28
- /** QUAL-01 质量维度。
29
- *
30
- * 优先用硬事件(retry_reflection / checklist_triggered / ask_human_call),
31
- * 这是 core.ts 在接缝处显式 emit 的,可信度高。
32
- * 如果 events 里没有对应硬事件(老 trace / 旧 fixture 走 history 文本回放),fallback
33
- * 到 history 文本扫描 + tool_call_end(name === 'ask_human', status === 'success')。
34
- *
35
- * - `retry_reflection` → 反思重试注入次数(RETRY-01)
36
- * - `checklist_triggered` → PROMPT-02 触发次数
37
- * - `ask_human_call` (status === 'success') → ASK-01 触发次数
38
- *
39
- * 三类都给出 fallback 0,确保报告维度永远是 number,不会被 NaN 污染。 */
40
- function reduceQualityDimensions(events, history) {
41
- // 1. 硬事件计数(优先级最高)。
42
- let reflectionRounds = 0;
43
- let checklistTriggered = 0;
44
- let askHumanCount = 0;
45
- let hasHardReflection = false;
46
- let hasHardChecklist = false;
47
- let hasHardAskHuman = false;
48
- for (const event of events) {
49
- if (event.type === 'retry_reflection') {
50
- reflectionRounds += 1;
51
- hasHardReflection = true;
52
- }
53
- else if (event.type === 'checklist_triggered') {
54
- checklistTriggered += 1;
55
- hasHardChecklist = true;
56
- }
57
- else if (event.type === 'ask_human_call' && event.data.status === 'success') {
58
- askHumanCount += 1;
59
- hasHardAskHuman = true;
60
- }
61
- }
62
- // 2. fallback: 历史事件没有硬信号时,从 history 文本 + tool_call_end 推断。
63
- // 保留旧 fixture / 回放 JSONL 的兼容能力。
64
- if (!hasHardReflection || !hasHardChecklist || !hasHardAskHuman) {
65
- if (!hasHardReflection) {
66
- const historyText = joinHistoryForMarkerScan(history);
67
- reflectionRounds += (historyText.match(/\[retry reflection:/g) ?? []).length;
68
- }
69
- if (!hasHardChecklist) {
70
- const historyText = joinHistoryForMarkerScan(history);
71
- checklistTriggered += (historyText.match(/\[checklist\]/g) ?? []).length;
72
- }
73
- if (!hasHardAskHuman) {
74
- for (const event of events) {
75
- if (event.type !== 'tool_call_end')
76
- continue;
77
- if (event.data.name === 'ask_human' && event.data.status === 'success') {
78
- askHumanCount += 1;
79
- }
80
- }
81
- }
82
- }
83
- return { reflectionRounds, askHumanCount, checklistTriggered };
2
+ function countAskHumanCalls(events) {
3
+ return events.filter((event) => event.type === 'ask_human_call' && event.data.status === 'success').length;
84
4
  }
85
- export function reduceTraceMetrics(events, history) {
5
+ export function reduceTraceMetrics(events) {
86
6
  const ends = events.filter((event) => event.type === 'tool_call_end');
87
7
  let recovered = false;
88
8
  let hadFailure = false;
89
9
  let successes = 0;
90
- let toolRetries = 0;
91
10
  let tokens = 0;
92
11
  let hasTokens = false;
93
12
  for (const event of ends) {
94
13
  const status = String(event.data.status ?? 'error');
95
- const retry = Number(event.data.retry ?? 0);
96
- toolRetries += Number.isFinite(retry) ? retry : 0;
97
14
  if (status === 'success') {
98
15
  successes++;
99
16
  if (hadFailure)
@@ -113,21 +30,17 @@ export function reduceTraceMetrics(events, history) {
113
30
  }
114
31
  }
115
32
  const modelRetries = events.filter((event) => event.type === 'model_retry').length;
116
- const firstValidation = events.find((event) => event.type === 'validation_end');
117
33
  const turnEnd = [...events].reverse().find((event) => event.type === 'turn_end');
118
- const quality = reduceQualityDimensions(events, history);
119
34
  return {
120
35
  toolCalls: events.filter((event) => event.type === 'tool_call_start').length,
121
36
  toolFailures: ends.length - successes,
122
37
  toolRecovery: recovered,
123
38
  firstSuccessRate: ends.length ? successes / ends.length : 1,
124
39
  modelRetries,
125
- toolRetries,
126
- retries: modelRetries + toolRetries,
40
+ retries: modelRetries,
127
41
  tokens: hasTokens ? tokens : null,
128
42
  durationMs: Number(turnEnd?.data.durationMs ?? 0),
129
- firstValidationPassed: firstValidation?.data.status === 'passed',
130
- ...quality,
43
+ askHumanCount: countAskHumanCalls(events),
131
44
  };
132
45
  }
133
46
  /** Reads event JSONL; malformed/legacy summary lines are ignored. */
@@ -41,14 +41,5 @@ export function appendCurrentSessionTrace(trace) {
41
41
  const sessionId = getCurrentSessionId();
42
42
  if (!sessionId)
43
43
  return;
44
- const validation = trace.validation
45
- ? {
46
- status: trace.validation.status,
47
- level: trace.validation.level,
48
- durationMs: trace.validation.durationMs,
49
- verificationComplete: trace.validation.verificationComplete,
50
- fingerprint: trace.validation.fingerprint,
51
- }
52
- : undefined;
53
- appendTraceLine(sessionId, { ...trace, sessionId, validation });
44
+ appendTraceLine(sessionId, { ...trace, sessionId });
54
45
  }