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.
- package/README.md +23 -37
- package/README.zh-CN.md +46 -38
- package/dist/agent/core.js +100 -446
- package/dist/agent/index.js +2 -21
- package/dist/agent/spawn.js +3 -5
- package/dist/agent/work-discipline.js +16 -70
- package/dist/config/index.js +6 -7
- package/dist/context/age-aware.js +18 -48
- package/dist/context/artifacts.js +19 -17
- package/dist/context/budget.js +27 -28
- package/dist/context/classifier.js +0 -1
- package/dist/context/encoders/index.js +4 -11
- package/dist/context/index.js +4 -7
- package/dist/context/lifecycle.js +115 -483
- package/dist/context/pipeline.js +8 -15
- package/dist/context/relevance.js +77 -55
- package/dist/host/stdio.js +0 -6
- package/dist/i18n/index.js +0 -6
- package/dist/index.js +11 -1
- package/dist/llm/index.js +72 -3
- package/dist/mcp/index.js +0 -1
- package/dist/repl/index.js +21 -15
- package/dist/runtime/browser-manager.js +299 -0
- package/dist/runtime/dev-server-manager.js +354 -0
- package/dist/runtime/shutdown.js +26 -0
- package/dist/session/compact.js +86 -102
- package/dist/session/index.js +0 -1
- package/dist/session/scheduler.js +88 -92
- package/dist/session/trace-metrics.js +5 -92
- package/dist/session/trace.js +1 -10
- package/dist/tools/builtins/browser.js +199 -0
- package/dist/tools/builtins/dev-server.js +99 -0
- package/dist/tools/builtins/index.js +28 -19
- package/dist/tools/builtins/screenshot.js +173 -0
- package/dist/tools/builtins/view-image.js +49 -0
- package/dist/tools/constants.js +3 -0
- package/dist/tools/registry.js +5 -29
- package/dist/ui/layout.js +28 -5
- package/dist/ui/render.js +11 -0
- package/package.json +2 -2
- package/dist/agent/middleware/checklist.js +0 -59
- package/dist/session/drop.d.ts +0 -19
- package/dist/session/drop.js +0 -93
- package/dist/tools/builtins/drop-context.d.ts +0 -18
- package/dist/tools/builtins/drop-context.js +0 -68
- package/dist/verification/diagnostics.js +0 -108
- package/dist/verification/fingerprint.js +0 -54
- package/dist/verification/index.js +0 -333
- package/dist/verification/postconditions.js +0 -98
- package/dist/verification/targeted-tests.js +0 -96
- package/dist/verification/types.js +0 -1
package/dist/session/compact.js
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
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
|
|
460
|
-
*
|
|
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
|
-
//
|
|
476
|
-
const needsCompact =
|
|
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
|
-
|
|
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:
|
|
471
|
+
threshold: DEFAULT_BUDGET_POLICY.pressureTriggerRatio,
|
|
489
472
|
focus: manualOpts?.focus,
|
|
490
473
|
manual: isManual,
|
|
491
|
-
force
|
|
474
|
+
// 硬闸触发时 force 直通保护区:首轮/当前轮不豁免。
|
|
475
|
+
force: manualOpts?.force === true || hardCap,
|
|
492
476
|
tools: activeTools,
|
|
493
477
|
contextState: state,
|
|
494
478
|
});
|
package/dist/session/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
32
|
+
// External file changes and mutations only update artifact metadata here.
|
|
33
33
|
refreshArtifactFreshness(state, history);
|
|
34
|
-
|
|
35
|
-
const
|
|
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
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
59
|
-
// 暴露给 /context 共享读(repl / context 命令)
|
|
73
|
+
scheduler.lastRunLog = log;
|
|
60
74
|
state.schedulerLog = log;
|
|
61
75
|
return historyRebuilt;
|
|
62
76
|
},
|
|
63
77
|
};
|
|
64
|
-
return
|
|
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
|
-
|
|
69
|
-
return s.runStep(history, step, activeTools);
|
|
81
|
+
return createBudgetScheduler(state).runStep(history, step, activeTools);
|
|
70
82
|
}
|
|
71
|
-
/**
|
|
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
|
-
|
|
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:
|
|
88
|
+
threshold: DEFAULT_BUDGET_POLICY.pressureTriggerRatio,
|
|
88
89
|
focus,
|
|
89
90
|
manual: true,
|
|
90
91
|
force: opts?.force,
|
|
91
92
|
}));
|
|
92
|
-
|
|
93
|
+
const report = evaluateBudget(history, config.contextWindowTokens, -1, contextState.correction);
|
|
94
|
+
const log = {
|
|
93
95
|
step: -1,
|
|
94
|
-
report
|
|
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:
|
|
100
|
-
estimateBefore:
|
|
101
|
-
estimateAfter:
|
|
102
|
-
protectedRatio:
|
|
103
|
-
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
|
-
|
|
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
|
-
|
|
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
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
126
|
-
retries: modelRetries + toolRetries,
|
|
40
|
+
retries: modelRetries,
|
|
127
41
|
tokens: hasTokens ? tokens : null,
|
|
128
42
|
durationMs: Number(turnEnd?.data.durationMs ?? 0),
|
|
129
|
-
|
|
130
|
-
...quality,
|
|
43
|
+
askHumanCount: countAskHumanCalls(events),
|
|
131
44
|
};
|
|
132
45
|
}
|
|
133
46
|
/** Reads event JSONL; malformed/legacy summary lines are ignored. */
|
package/dist/session/trace.js
CHANGED
|
@@ -41,14 +41,5 @@ export function appendCurrentSessionTrace(trace) {
|
|
|
41
41
|
const sessionId = getCurrentSessionId();
|
|
42
42
|
if (!sessionId)
|
|
43
43
|
return;
|
|
44
|
-
|
|
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
|
}
|