mocode-ai 1.1.7 → 1.1.9
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 +45 -40
- package/README.zh-CN.md +51 -41
- package/dist/agent/core.js +137 -447
- package/dist/agent/index.js +5 -24
- package/dist/agent/spawn.js +5 -5
- package/dist/agent/work-discipline.js +16 -70
- package/dist/config/index.js +87 -33
- 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 +20 -6
- package/dist/index.js +11 -1
- package/dist/llm/index.js +82 -9
- package/dist/mcp/index.js +0 -1
- package/dist/repl/index.js +69 -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/notes.js +107 -0
- 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 +33 -19
- package/dist/tools/builtins/plan-update.js +144 -0
- package/dist/tools/builtins/screenshot.js +173 -0
- package/dist/tools/builtins/view-image.js +49 -0
- package/dist/tools/constants.js +38 -8
- package/dist/tools/registry.js +5 -29
- package/dist/ui/batch.js +3 -0
- package/dist/ui/content.js +19 -16
- package/dist/ui/layout.js +120 -46
- 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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// session/notes.ts - Session Notepad(notes.md)的 plan 状态单一事实源。
|
|
2
|
+
// 负责:canonical 文件路径、plan 结构化模型、markdown 渲染、以及"只替换活跃 ## Plan: 段、
|
|
3
|
+
// 保留其它笔记段"的写入。被 plan_update 工具、agent core(事件驱动重同步/nag)、
|
|
4
|
+
// config(notepad 索引 / compact 重注入)共用,避免路径与段解析逻辑散落多处。
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { getSandboxRoot } from '../sandbox/root.js';
|
|
8
|
+
import { getCurrentSessionId } from './state.js';
|
|
9
|
+
/** 单个活跃 plan 允许的最大步骤数(防无限膨胀,对齐 Claude Code TodoWrite 的 20 上限)。 */
|
|
10
|
+
export const PLAN_MAX_STEPS = 20;
|
|
11
|
+
/** 当前会话 notes.md 的绝对路径;无会话时返 null。 */
|
|
12
|
+
export function getNotesFilePath(sessionId = getCurrentSessionId()) {
|
|
13
|
+
if (!sessionId)
|
|
14
|
+
return null;
|
|
15
|
+
const root = getSandboxRoot() ?? process.cwd();
|
|
16
|
+
return path.join(root, '.mocode', 'sessions', sessionId, 'notes.md');
|
|
17
|
+
}
|
|
18
|
+
/** notes.md 的 mtime(ms),不存在或不可读返 null。供 core 判断"本步是否改动了 notes.md"。 */
|
|
19
|
+
export function getNotesMtime(sessionId = getCurrentSessionId()) {
|
|
20
|
+
const p = getNotesFilePath(sessionId);
|
|
21
|
+
if (!p)
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
return fs.statSync(p).mtimeMs;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 把 plan 渲染为 canonical markdown 段(不含首尾多余空行)。
|
|
32
|
+
* 复选框格式 `- [ ] N.` / `- [x] N.` 与 repl 状态栏 chip 的计数正则严格对齐;
|
|
33
|
+
* in_progress 步骤追加 ` ◀ 当前` 后缀(仍计为未完成),让 compact 重注入后模型能直接续上。
|
|
34
|
+
* 全部完成时标题写为 `## Done:`(自动结算),extractActivePlanSection 随之返回 null。
|
|
35
|
+
*/
|
|
36
|
+
export function renderPlanSection(plan) {
|
|
37
|
+
const allDone = plan.steps.length > 0 && plan.steps.every((s) => s.status === 'completed');
|
|
38
|
+
const header = allDone ? `## Done: ${plan.title}` : `## Plan: ${plan.title}`;
|
|
39
|
+
const lines = [header];
|
|
40
|
+
if (plan.goal)
|
|
41
|
+
lines.push(`Goal: ${plan.goal}`);
|
|
42
|
+
lines.push('', '### Steps');
|
|
43
|
+
plan.steps.forEach((s, i) => {
|
|
44
|
+
const box = s.status === 'completed' ? '[x]' : '[ ]';
|
|
45
|
+
const suffix = s.status === 'in_progress'
|
|
46
|
+
? ` ◀ ${(s.activeForm ?? '').trim() || 'current'}`
|
|
47
|
+
: '';
|
|
48
|
+
lines.push(`- ${box} ${i + 1}. ${s.content}${suffix}`);
|
|
49
|
+
});
|
|
50
|
+
const done = plan.steps.filter((s) => s.status === 'completed').length;
|
|
51
|
+
lines.push('', '### Progress', `- ${done}/${plan.steps.length} steps complete`);
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
/** 读取当前活跃 `## Plan:` 段的标题;无活跃 plan 返 null。 */
|
|
55
|
+
export function readActivePlanTitle(sessionId = getCurrentSessionId()) {
|
|
56
|
+
const p = getNotesFilePath(sessionId);
|
|
57
|
+
if (!p)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const normalized = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
|
|
61
|
+
const headerLine = normalized.split('\n').find((l) => /^## Plan:\s*.+$/.test(l));
|
|
62
|
+
return headerLine?.match(/^## Plan:\s*(.+)$/)?.[1].trim() ?? null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 把 plan 写入 notes.md:替换已有活跃 `## Plan:` 段(原位),否则插到文件顶部;
|
|
70
|
+
* 其它所有笔记段(findings / Open Questions / 已结算 ## Done:)原样保留。
|
|
71
|
+
* 文件不存在则创建(含父目录)。返回 { path, settled } 或 { error }。
|
|
72
|
+
*/
|
|
73
|
+
export function writePlanToNotes(plan, sessionId = getCurrentSessionId()) {
|
|
74
|
+
const p = getNotesFilePath(sessionId);
|
|
75
|
+
if (!p)
|
|
76
|
+
return { error: 'no active session' };
|
|
77
|
+
const section = renderPlanSection(plan);
|
|
78
|
+
let existing = '';
|
|
79
|
+
try {
|
|
80
|
+
existing = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
existing = '';
|
|
84
|
+
}
|
|
85
|
+
let next;
|
|
86
|
+
const lines = existing.split('\n');
|
|
87
|
+
const start = lines.findIndex((l) => /^## Plan:\s*.+$/.test(l));
|
|
88
|
+
if (start >= 0) {
|
|
89
|
+
const endOffset = lines.slice(start + 1).findIndex((l) => /^##\s/.test(l));
|
|
90
|
+
const end = endOffset < 0 ? lines.length : start + 1 + endOffset;
|
|
91
|
+
const before = lines.slice(0, start).join('\n').replace(/\s+$/, '');
|
|
92
|
+
const after = lines.slice(end).join('\n').replace(/^\s+/, '');
|
|
93
|
+
next = [before, section, after].filter((s) => s.length > 0).join('\n\n') + '\n';
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const rest = existing.trim();
|
|
97
|
+
next = rest ? `${section}\n\n${rest}\n` : `${section}\n`;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
101
|
+
fs.writeFileSync(p, next, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
105
|
+
}
|
|
106
|
+
return { path: p, settled: plan.steps.length > 0 && plan.steps.every((s) => s.status === 'completed') };
|
|
107
|
+
}
|
|
@@ -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
|
}
|