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/tools/registry.js
CHANGED
|
@@ -2,7 +2,6 @@ import { builtinTools } from './builtins/index.js';
|
|
|
2
2
|
import { beginPathMutation, beginWorkspaceMutation, endPathMutation, endWorkspaceMutation, getCurrentTurnMutationState, } from '../rollback/index.js';
|
|
3
3
|
import { enforceSandbox } from '../sandbox/index.js';
|
|
4
4
|
import { resolveResourceLockRequests, toolResourceLockManager } from './resource-lock.js';
|
|
5
|
-
import { executeWithToolRetry } from './retry.js';
|
|
6
5
|
import { validateToolArguments } from './validation.js';
|
|
7
6
|
import { t } from '../i18n/index.js';
|
|
8
7
|
import { isToolErrorOutput } from './result.js';
|
|
@@ -40,7 +39,6 @@ function rebuildTools() {
|
|
|
40
39
|
const DEFAULT_CAPABILITIES = Object.freeze({
|
|
41
40
|
effect: 'unknown',
|
|
42
41
|
concurrency: 'serial',
|
|
43
|
-
retry: 'never',
|
|
44
42
|
});
|
|
45
43
|
export function findTool(name) {
|
|
46
44
|
return tools.find((tool) => tool.name === name);
|
|
@@ -132,8 +130,8 @@ function executionErrorOutcome(name, error, startedAt, changedFiles) {
|
|
|
132
130
|
durationMs: Date.now() - startedAt,
|
|
133
131
|
};
|
|
134
132
|
}
|
|
135
|
-
/**
|
|
136
|
-
async function
|
|
133
|
+
/** Execute one tool call while holding its declared locks and capturing rollback state. */
|
|
134
|
+
async function executeToolOnce(tool, args, signal, opts) {
|
|
137
135
|
const startedAt = Date.now();
|
|
138
136
|
const capabilities = getToolCapabilities(tool);
|
|
139
137
|
let mutationVersionBefore;
|
|
@@ -141,8 +139,7 @@ async function executeToolAttempt(tool, args, signal, opts, notifyLockAcquired)
|
|
|
141
139
|
try {
|
|
142
140
|
const requests = resolveResourceLockRequests(capabilities, args);
|
|
143
141
|
return await toolResourceLockManager.withLocks(requests, signal, async () => {
|
|
144
|
-
|
|
145
|
-
opts?.onLockAcquired?.(args);
|
|
142
|
+
opts?.onLockAcquired?.(args);
|
|
146
143
|
const mutationBefore = getCurrentTurnMutationState();
|
|
147
144
|
mutationVersionBefore = mutationBefore.version;
|
|
148
145
|
// Transactional tools own their full write-set capture inside ChangeSet commit.
|
|
@@ -156,7 +153,7 @@ async function executeToolAttempt(tool, args, signal, opts, notifyLockAcquired)
|
|
|
156
153
|
: null;
|
|
157
154
|
let raw;
|
|
158
155
|
try {
|
|
159
|
-
raw = await tool.execute(args, { signal
|
|
156
|
+
raw = await tool.execute(args, { signal });
|
|
160
157
|
}
|
|
161
158
|
finally {
|
|
162
159
|
if (pathCapture)
|
|
@@ -190,17 +187,6 @@ async function executeToolAttempt(tool, args, signal, opts, notifyLockAcquired)
|
|
|
190
187
|
return executionErrorOutcome(tool.name, error, startedAt, changedFiles);
|
|
191
188
|
}
|
|
192
189
|
}
|
|
193
|
-
function stableJson(value) {
|
|
194
|
-
if (Array.isArray(value))
|
|
195
|
-
return `[${value.map(stableJson).join(',')}]`;
|
|
196
|
-
if (value && typeof value === 'object') {
|
|
197
|
-
return `{${Object.entries(value)
|
|
198
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
199
|
-
.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
|
|
200
|
-
.join(',')}}`;
|
|
201
|
-
}
|
|
202
|
-
return JSON.stringify(value) ?? 'null';
|
|
203
|
-
}
|
|
204
190
|
/**
|
|
205
191
|
* 结构化工具调度入口。永不抛错;旧字符串工具在此归一化为 ToolOutcome。
|
|
206
192
|
* 权限仍由 Agent 在展示工具头之前预检,保持现有交互时序。
|
|
@@ -226,21 +212,11 @@ export async function executeToolOutcome(name, argsRaw, signal, opts) {
|
|
|
226
212
|
return terminalOutcome('error', validation.code, `错误:工具 ${name} 参数无效: ${validation.message}`, startedAt);
|
|
227
213
|
}
|
|
228
214
|
const args = parsed;
|
|
229
|
-
const fingerprint = `${name}\x00${stableJson(args)}`;
|
|
230
215
|
const sandboxError = enforceSandbox(name, args);
|
|
231
216
|
if (sandboxError) {
|
|
232
217
|
return terminalOutcome('denied', 'SANDBOX_DENIED', sandboxError, startedAt);
|
|
233
218
|
}
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
return await executeWithToolRetry(capabilities, fingerprint, signal, (attempt) => executeToolAttempt(tool, args, signal, opts, attempt === 1), opts?.onRetry);
|
|
237
|
-
}
|
|
238
|
-
catch (error) {
|
|
239
|
-
if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {
|
|
240
|
-
return terminalOutcome('aborted', 'ABORTED', t('command.interrupted'), startedAt);
|
|
241
|
-
}
|
|
242
|
-
return executionErrorOutcome(name, error, startedAt, []);
|
|
243
|
-
}
|
|
219
|
+
return executeToolOnce(tool, args, signal, opts);
|
|
244
220
|
}
|
|
245
221
|
/** 字符串兼容入口:现有调用方、TUI 和 LLM history 无需同步迁移。 */
|
|
246
222
|
export async function executeTool(name, argsRaw, signal, opts) {
|
package/dist/ui/layout.js
CHANGED
|
@@ -75,6 +75,10 @@ let scrollOffset = 0; // 滚动回看距尾行数(0=尾,跟随新内容);>0 时
|
|
|
75
75
|
let scrollLockUntil = 0; // 发消息轮首滚动锁(绝对时间戳 ms,0=未锁):吸收 stdin 残留滚轮事件,防 resetScroll 回尾后被重新滚上去
|
|
76
76
|
const SCROLL_LOCK_MS = 400; // 锁时长:覆盖 OS 缓冲残留 + 常规滚轮惯性;LLM TTFB 多 >200ms,不影响轮中后段滚动
|
|
77
77
|
let base = null;
|
|
78
|
+
// 运行态实时 token 用量(agent core 流式推送,轮末 repl 清 undefined)。
|
|
79
|
+
// composeModelLine 在 RUNNING 态把它画成 chip 放 context 进度条左侧;
|
|
80
|
+
// 不主动触发重画——RUNNING 态 turnTimer 200ms 心跳重画自然取最新值。
|
|
81
|
+
let liveUsage;
|
|
78
82
|
let statusText = '';
|
|
79
83
|
let spinnerFrame;
|
|
80
84
|
let turnStart = null; // RUNNING 态起点(Date.now());INPUT 态为 null。composeStatus 据此拼走时。
|
|
@@ -1394,14 +1398,18 @@ function composeModelLine(status, cols) {
|
|
|
1394
1398
|
+ hintW
|
|
1395
1399
|
+ ((hintPart && tokChip) ? sepHT.length : 0)
|
|
1396
1400
|
+ tokW;
|
|
1397
|
-
//
|
|
1401
|
+
// 右段:实时用量 chip(仅 RUNNING)+ ctx + sep + cwd,右端对齐。cwd 按预算截断,极窄(<6)隐藏。
|
|
1402
|
+
// 实时 chip 放 context 进度条左侧:本轮累计 ↑prompt ↓completion,流式实时增长。
|
|
1398
1403
|
// 任一 chip 极宽时收紧 cwd(toolbar 列挤压场景),先从 cwd 砍、再隐藏 cwd、再按 hint→chip 顺序省。
|
|
1404
|
+
const liveChip = mode === 'running' && liveUsage ? formatLiveUsageChip(liveUsage) : '';
|
|
1405
|
+
const liveW = displayWidth(stripAnsi(liveChip));
|
|
1406
|
+
const liveSepW = liveChip ? STATUS_SEP_W : 0;
|
|
1399
1407
|
const minGap = 2;
|
|
1400
|
-
let cwdBudget = cols - leftW - minGap - ctxW - STATUS_SEP_W - 1;
|
|
1408
|
+
let cwdBudget = cols - leftW - minGap - liveW - liveSepW - ctxW - STATUS_SEP_W - 1;
|
|
1401
1409
|
let cwd = cwdBudget >= 6 ? truncateDisplay(status.cwd, cwdBudget) : '';
|
|
1402
1410
|
let cwdW = displayWidth(cwd);
|
|
1403
|
-
let rightStr = `${ctx}${STATUS_SEP}${ui.dim}${cwd}${ui.reset}`;
|
|
1404
|
-
let rightW = ctxW + STATUS_SEP_W + cwdW;
|
|
1411
|
+
let rightStr = `${liveChip}${liveChip ? STATUS_SEP : ''}${ctx}${STATUS_SEP}${ui.dim}${cwd}${ui.reset}`;
|
|
1412
|
+
let rightW = liveW + liveSepW + ctxW + STATUS_SEP_W + cwdW;
|
|
1405
1413
|
// 极窄:逐步降级——先藏 hint,再藏 token chip,只剩 modeTag 与右段挤。
|
|
1406
1414
|
// 这样 80 列宽终端下 hint 和 chip 都能稳住,只 <50 列才退化到只剩 modeTag。
|
|
1407
1415
|
if (leftW + minGap + rightW > cols) {
|
|
@@ -1430,11 +1438,21 @@ function formatTurnTokenChip(usage) {
|
|
|
1430
1438
|
const text = n < 1000 ? `${n}` : `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`;
|
|
1431
1439
|
const cached = usage.cachedTokens ?? 0;
|
|
1432
1440
|
const cacheTag = cached > 0
|
|
1433
|
-
? `
|
|
1441
|
+
? ` ↻ ${cached < 1000 ? cached : `${(cached / 1000).toFixed(cached >= 10000 ? 0 : 1)}k`}`
|
|
1434
1442
|
: '';
|
|
1435
1443
|
// chip 用 mid 灰(降优先级)— 模式仍是主色
|
|
1436
1444
|
return `${ui.dim}${text} tokens${cacheTag}${ui.reset}`;
|
|
1437
1445
|
}
|
|
1446
|
+
/** 运行态实时用量 chip(放 context 进度条左侧):↑计费prompt(裸-cached,与轮末摘要 (↑…) 同口径)
|
|
1447
|
+
* ↓completion,cache 命中带 ↻ 标记;缩写与轮末摘要 / 左侧 turn chip 同款。
|
|
1448
|
+
* 流式期为估算值,每步末尾 usage chunk 到达后换实测;dim 灰降优先级,不与进度条阈值警示抢色。 */
|
|
1449
|
+
function formatLiveUsageChip(u) {
|
|
1450
|
+
const fmt = (n) => (n < 1000 ? `${n}` : `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`);
|
|
1451
|
+
const cached = u.cachedTokens ?? 0;
|
|
1452
|
+
const billable = Math.max(0, u.promptTokens - cached);
|
|
1453
|
+
const cacheTag = cached > 0 ? ` ↻ ${fmt(cached)}` : '';
|
|
1454
|
+
return `${ui.dim}↑ ${fmt(billable)} ↓ ${fmt(u.completionTokens)}${cacheTag}${ui.reset}`;
|
|
1455
|
+
}
|
|
1438
1456
|
/** spinner 行上方的「虚拟空行」(contentBottom+1)。
|
|
1439
1457
|
* - 有活跃 plan:显「plan: <summary> ▸ N. step」整行左对齐(yellow + dim)
|
|
1440
1458
|
* - 无活跃 plan:空(保留原分隔视觉,避免内容贴输入区)
|
|
@@ -1589,6 +1607,11 @@ export function clearLiveAtCursor() {
|
|
|
1589
1607
|
frameRow = 0;
|
|
1590
1608
|
frameCol = 0;
|
|
1591
1609
|
}
|
|
1610
|
+
/** 推送 / 清空运行态实时 token 用量(agent core 流式推送;repl 轮末清 undefined)。
|
|
1611
|
+
* 不触发重画:RUNNING 态 turnTimer 200ms 心跳重画状态行,自然取到最新值。 */
|
|
1612
|
+
export function setLiveUsage(u) {
|
|
1613
|
+
liveUsage = u;
|
|
1614
|
+
}
|
|
1592
1615
|
/** 更新状态行基线(模型 / context / cwd / 模式标识 / 活跃 plan chip / 本轮 token chip)。repl 在轮次边界与切模式时调。 */
|
|
1593
1616
|
export function setStatusBase(b) {
|
|
1594
1617
|
base = b;
|
package/dist/ui/render.js
CHANGED
|
@@ -318,6 +318,17 @@ export function summarizeToolCall(name, argsRaw) {
|
|
|
318
318
|
return s('path') || truncateDisplay(argsRaw, 80);
|
|
319
319
|
case 'run_command':
|
|
320
320
|
return truncateDisplay(s('command') || argsRaw, 100);
|
|
321
|
+
case 'dev_server': {
|
|
322
|
+
const action = s('action');
|
|
323
|
+
const detail = s('command') || s('id') || s('readyUrl');
|
|
324
|
+
return truncateDisplay(detail ? `${action} · ${detail}` : action || argsRaw, 100);
|
|
325
|
+
}
|
|
326
|
+
case 'browser': {
|
|
327
|
+
// 刻意不显示 fill 的 value:可能是密码等敏感输入。
|
|
328
|
+
const action = s('action');
|
|
329
|
+
const detail = s('url') || s('selector') || s('key') || s('sessionId');
|
|
330
|
+
return truncateDisplay(detail ? `${action} · ${detail}` : action || argsRaw, 100);
|
|
331
|
+
}
|
|
321
332
|
case 'glob':
|
|
322
333
|
return s('pattern') || argsRaw;
|
|
323
334
|
case 'grep': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"dotenv": "^16.0.0",
|
|
34
34
|
"fast-glob": "^3.0.0",
|
|
35
35
|
"openai": "^4.0.0",
|
|
36
|
+
"playwright": "1.62.1",
|
|
36
37
|
"ws": "8.21.0"
|
|
37
38
|
},
|
|
38
|
-
"optionalDependencies": {},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.0.0",
|
|
41
41
|
"@types/ws": "8.5.13",
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// PROMPT-02: PreCompletionChecklistMiddleware.
|
|
2
|
-
//
|
|
3
|
-
// 在 agent 宣告完成(no tool calls + 有 candidate 正文)的那一刻,向 history
|
|
4
|
-
// 推一条 user 消息,让模型再走一轮对 5 项 checklist 做显式确认 —— 这是
|
|
5
|
-
// "硬关卡",与 PROMPT-01 的 4 阶段"软纪律"叠加。
|
|
6
|
-
//
|
|
7
|
-
// 关键约束:
|
|
8
|
-
// - 纯函数(handler / buildUserMessage) → 易测、易 opt-out。
|
|
9
|
-
// - 5 项 checklist 是稳定的字符串数组,fixture 可直接断言关键词。
|
|
10
|
-
// - 复用 PROMPT-01 核心文本中的 "verification is a hard prerequisite" 锚点
|
|
11
|
-
// 风格(不另起炉灶),但用单数第一人称直白的 checklist 措辞。
|
|
12
|
-
// - 与 thrash-tracker 风格正交:trash 是 hint(LLM 仍可继续 retry),checklist
|
|
13
|
-
// 是 gate(LLM 必须覆盖才能 done)。
|
|
14
|
-
// - opt-out 显式:plan 模式 / 调用方传 `preCompletionChecklist: false`。
|
|
15
|
-
/** 5 项 checklist 内容(稳定,fixture 直接断言)。措辞对齐 LangChain 实证。 */
|
|
16
|
-
export const CHECKLIST_ITEMS = [
|
|
17
|
-
'Did I actually run a command/test/compile, or did I just re-read my own code?',
|
|
18
|
-
'Does the output match the original spec, not "what I thought I wrote"?',
|
|
19
|
-
'Did I exercise the boundary I claim to have covered (not just the happy path)?',
|
|
20
|
-
'Does the existing test suite still pass?',
|
|
21
|
-
'If I cannot verify, did I tell the user explicitly instead of pretending to be done?',
|
|
22
|
-
];
|
|
23
|
-
/**
|
|
24
|
-
* 拼出 checklist user 消息。固定 5 项 + 硬规则;以 `[checklist]` 起头
|
|
25
|
-
* 便于 LLM 识别为强制二次确认(也便于 eval fixture 关键字定位)。
|
|
26
|
-
*/
|
|
27
|
-
export function buildChecklistUserMessage(modelFamily) {
|
|
28
|
-
const opener = modelFamily === 'anthropic'
|
|
29
|
-
? 'Verification gate — you have not actually run a command that exercises the change against the spec. Before declaring done, answer each item below explicitly:'
|
|
30
|
-
: modelFamily === 'openai'
|
|
31
|
-
? 'MANDATORY pre-completion checklist. You MUST address every item below before your final reply:'
|
|
32
|
-
: 'Pre-completion checklist — answer each item explicitly before declaring done:';
|
|
33
|
-
const items = CHECKLIST_ITEMS.map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
34
|
-
return `[checklist] ${opener}
|
|
35
|
-
|
|
36
|
-
${items}
|
|
37
|
-
|
|
38
|
-
Hard rule: "I read the code and it looks right" is not a completion signal. If you cannot run a verification, say so explicitly in your final reply. Do not paraphrase this checklist back as the answer — provide the actual evidence (which command, which output, which spec line it satisfied).
|
|
39
|
-
|
|
40
|
-
Bonus (ASK-01): Before declaring done, also answer: am I guessing any fact the user did not state? If yes, surface the guess to the user in your final reply or call \`ask_human\` (within the per-turn budget) — never silently guess on a non-reversible choice.`;
|
|
41
|
-
}
|
|
42
|
-
/** 默认 trigger 条件:有 mutation + 没工具调用 + 验证未通过或没跑过 + 非 plan。 */
|
|
43
|
-
export const defaultChecklistHandler = (ctx) => {
|
|
44
|
-
if (ctx.mode === 'plan')
|
|
45
|
-
return false;
|
|
46
|
-
if (!ctx.hadMutation)
|
|
47
|
-
return false;
|
|
48
|
-
// 已经通过验证:放行,不再二次确认(避免噪声)。
|
|
49
|
-
if (ctx.lastValidationStatus === 'passed')
|
|
50
|
-
return false;
|
|
51
|
-
return true;
|
|
52
|
-
};
|
|
53
|
-
export function createPreCompletionChecklistMiddleware() {
|
|
54
|
-
return {
|
|
55
|
-
handler: defaultChecklistHandler,
|
|
56
|
-
buildUserMessage: buildChecklistUserMessage,
|
|
57
|
-
items: CHECKLIST_ITEMS,
|
|
58
|
-
};
|
|
59
|
-
}
|
package/dist/session/drop.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ChatMessage } from '../llm/index.js';
|
|
2
|
-
import { estimateTokens } from '../llm/index.js';
|
|
3
|
-
import type { DropContextFilter, DropContextResult } from '../tools/types.js';
|
|
4
|
-
/**
|
|
5
|
-
* 剔除历史里命中的旧 tool 结果(原地修改 history)。
|
|
6
|
-
*
|
|
7
|
-
* 筛选(各维度 AND 组合):
|
|
8
|
-
* - toolNames:只剔除这些工具名的结果(空 = 不限)
|
|
9
|
-
* - contains:只剔除内容包含所有这些词(AND、大小写不敏感)的结果(空 = 不限)
|
|
10
|
-
*
|
|
11
|
-
* 保护:history[0](system)+ 当前轮(最后一个 user 及其之后)永不剔除。
|
|
12
|
-
* 已是存根的 tool 消息(含「已剔除」标记)不重复剔除(幂等)。
|
|
13
|
-
*
|
|
14
|
-
* 永不抛错;无匹配返 dropped=0。
|
|
15
|
-
*/
|
|
16
|
-
export declare function dropContextFromHistory(history: ChatMessage[], filter: DropContextFilter): DropContextResult;
|
|
17
|
-
/** 给 drop_context 工具结果格式化人类可读摘要(回灌给 agent)。 */
|
|
18
|
-
export declare function formatDropResult(r: DropContextResult): string;
|
|
19
|
-
export { estimateTokens };
|
package/dist/session/drop.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// 运行中上下文剔除(drop_context 工具的核心):把历史里无关的 tool 结果替换为存根。
|
|
2
|
-
//
|
|
3
|
-
// 与 compact 的区别:compact 是阈值触发的整体压缩(微截 + 摘要),drop_context 是 agent 主动、
|
|
4
|
-
// 精准剔除"已判定无关"的具体 tool 结果——agent 检索到大量无关信息后主动调用,释放上下文。
|
|
5
|
-
//
|
|
6
|
-
// 不变量(对齐 compact.ts):
|
|
7
|
-
// - 永不动 history[0](system prompt)。
|
|
8
|
-
// - 永不动当前轮:从末尾向前找到最后一个 user 消息,该 user 及其之后的 tool 结果一律保留
|
|
9
|
-
// (agent 本轮还在用,踢了会丢失正在进行的上下文)。
|
|
10
|
-
// - tool_call_id 配对:只改 tool 消息的 content,不删消息、不动 tool_calls 数组结构、不改 id。
|
|
11
|
-
// - 原地修改 history(同 compact:length=0;push 重建,repl 持有同一引用)。
|
|
12
|
-
// - 永不抛错(对齐「调度器永不抛错」契约);无匹配 / 无可剔除 → 返 dropped=0。
|
|
13
|
-
import { messageTokens, estimateTokens, } from '../llm/index.js';
|
|
14
|
-
import { lastUserIndex, toText, toolNameOf } from '../context/utils.js';
|
|
15
|
-
/**
|
|
16
|
-
* 剔除历史里命中的旧 tool 结果(原地修改 history)。
|
|
17
|
-
*
|
|
18
|
-
* 筛选(各维度 AND 组合):
|
|
19
|
-
* - toolNames:只剔除这些工具名的结果(空 = 不限)
|
|
20
|
-
* - contains:只剔除内容包含所有这些词(AND、大小写不敏感)的结果(空 = 不限)
|
|
21
|
-
*
|
|
22
|
-
* 保护:history[0](system)+ 当前轮(最后一个 user 及其之后)永不剔除。
|
|
23
|
-
* 已是存根的 tool 消息(含「已剔除」标记)不重复剔除(幂等)。
|
|
24
|
-
*
|
|
25
|
-
* 永不抛错;无匹配返 dropped=0。
|
|
26
|
-
*/
|
|
27
|
-
export function dropContextFromHistory(history, filter) {
|
|
28
|
-
const toolNames = filter.toolNames && filter.toolNames.length > 0
|
|
29
|
-
? new Set(filter.toolNames)
|
|
30
|
-
: null;
|
|
31
|
-
const contains = filter.contains && filter.contains.length > 0
|
|
32
|
-
? filter.contains.map((s) => s.toLowerCase())
|
|
33
|
-
: null;
|
|
34
|
-
// 当前轮保护区:最后一个 user 及其之后一律保留(agent 还在用)。
|
|
35
|
-
const guard = lastUserIndex(history);
|
|
36
|
-
// guard <= 0 表示无 user 或 user 就是 history[0](不会):整段历史都可剔除(除 history[0])。
|
|
37
|
-
const protectedFrom = guard > 0 ? guard : 0; // < protectedFrom 的才可剔除(即 [1, protectedFrom)
|
|
38
|
-
const items = [];
|
|
39
|
-
let freedTokens = 0;
|
|
40
|
-
const STUB_PREFIX = '⌦[已剔除:与当前任务无关]';
|
|
41
|
-
for (let i = 1; i < protectedFrom; i++) {
|
|
42
|
-
const m = history[i];
|
|
43
|
-
if (m.role !== 'tool')
|
|
44
|
-
continue;
|
|
45
|
-
const content = toText(m.content);
|
|
46
|
-
// 幂等:已是存根(含标记)不重复剔除。
|
|
47
|
-
if (content.startsWith(STUB_PREFIX))
|
|
48
|
-
continue;
|
|
49
|
-
// 维度 1:工具名
|
|
50
|
-
const tname = toolNameOf(history, i);
|
|
51
|
-
if (toolNames && (!tname || !toolNames.has(tname)))
|
|
52
|
-
continue;
|
|
53
|
-
// 维度 2:内容关键词(AND)
|
|
54
|
-
if (contains) {
|
|
55
|
-
const lower = content.toLowerCase();
|
|
56
|
-
if (!contains.every((kw) => lower.includes(kw)))
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
// 命中 → 替换为存根(保 tool_call_id 不动,只改 content)
|
|
60
|
-
const before = messageTokens(m);
|
|
61
|
-
const id = m.tool_call_id ?? '';
|
|
62
|
-
const stub = `${STUB_PREFIX} 原 ${tname ?? 'tool'} 结果(${content.length} 字符,约 ${before} tokens)${id ? ` · id …${id.slice(-6)}` : ''}⌫`;
|
|
63
|
-
m.content = stub;
|
|
64
|
-
const after = messageTokens(m);
|
|
65
|
-
freedTokens += Math.max(0, before - after);
|
|
66
|
-
items.push({
|
|
67
|
-
toolName: tname ?? 'tool',
|
|
68
|
-
toolCallId: id.slice(-6),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
dropped: items.length,
|
|
73
|
-
freedTokens,
|
|
74
|
-
items,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
/** 给 drop_context 工具结果格式化人类可读摘要(回灌给 agent)。 */
|
|
78
|
-
export function formatDropResult(r) {
|
|
79
|
-
if (r.dropped === 0) {
|
|
80
|
-
return '未剔除任何工具结果(无匹配的旧 tool 消息,或均在当前轮保护区内不可剔除)。';
|
|
81
|
-
}
|
|
82
|
-
const lines = [
|
|
83
|
-
`已剔除 ${r.dropped} 条无关工具结果,释放约 ${r.freedTokens} tokens。`,
|
|
84
|
-
'被剔除项(已替换为存根,tool_call_id 配对不变):',
|
|
85
|
-
];
|
|
86
|
-
for (const it of r.items) {
|
|
87
|
-
lines.push(` - ${it.toolName} (id …${it.toolCallId})`);
|
|
88
|
-
}
|
|
89
|
-
lines.push('这些结果在后续上下文中仅保留存根标记,不再占用篇幅。');
|
|
90
|
-
return lines.join('\n');
|
|
91
|
-
}
|
|
92
|
-
// 供 drop_context 工具估算用(避免直接 import llm 的公开 API 造成耦合,这里重导出)。
|
|
93
|
-
export { estimateTokens };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { Tool } from '../types.js';
|
|
2
|
-
/**
|
|
3
|
-
* 运行中上下文剔除工具:agent 检索到大量无关信息后,主动把历史里无关的 tool 结果替换为存根,
|
|
4
|
-
* 释放上下文空间(保 tool_call_id 配对不变量,只改 content)。
|
|
5
|
-
*
|
|
6
|
-
* 与 compact 的区别:compact 是阈值触发的整体压缩(微截 + 摘要);drop_context 是 agent 主动、
|
|
7
|
-
* 精准剔除"已判定无关"的具体 tool 结果。
|
|
8
|
-
*
|
|
9
|
-
* 保护:history[0](system)与当前轮(最后一个 user 及其之后)永不剔除——agent 还在用。
|
|
10
|
-
* 已是存根的不重复剔除(幂等)。永不抛错。
|
|
11
|
-
*
|
|
12
|
-
* 筛选(各维度 AND 组合,全部可选;不传 = 剔除所有可剔除的旧 tool 结果):
|
|
13
|
-
* - toolNames:只剔除这些工具名的结果(如 ["grep","read_file"])
|
|
14
|
-
* - contains:只剔除内容包含所有这些词(AND、大小写不敏感)的结果
|
|
15
|
-
*
|
|
16
|
-
* plan 模式不禁用:纯上下文管理,无文件 / 命令副作用。
|
|
17
|
-
*/
|
|
18
|
-
export declare const dropContextTool: Tool;
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// ---------- drop_context ----------
|
|
2
|
-
/**
|
|
3
|
-
* 运行中上下文剔除工具:agent 检索到大量无关信息后,主动把历史里无关的 tool 结果替换为存根,
|
|
4
|
-
* 释放上下文空间(保 tool_call_id 配对不变量,只改 content)。
|
|
5
|
-
*
|
|
6
|
-
* 与 compact 的区别:compact 是阈值触发的整体压缩(微截 + 摘要);drop_context 是 agent 主动、
|
|
7
|
-
* 精准剔除"已判定无关"的具体 tool 结果。
|
|
8
|
-
*
|
|
9
|
-
* 保护:history[0](system)与当前轮(最后一个 user 及其之后)永不剔除——agent 还在用。
|
|
10
|
-
* 已是存根的不重复剔除(幂等)。永不抛错。
|
|
11
|
-
*
|
|
12
|
-
* 筛选(各维度 AND 组合,全部可选;不传 = 剔除所有可剔除的旧 tool 结果):
|
|
13
|
-
* - toolNames:只剔除这些工具名的结果(如 ["grep","read_file"])
|
|
14
|
-
* - contains:只剔除内容包含所有这些词(AND、大小写不敏感)的结果
|
|
15
|
-
*
|
|
16
|
-
* plan 模式不禁用:纯上下文管理,无文件 / 命令副作用。
|
|
17
|
-
*/
|
|
18
|
-
export const dropContextTool = {
|
|
19
|
-
name: 'drop_context',
|
|
20
|
-
description: [
|
|
21
|
-
'Drop (stub-replace) irrelevant OLDER tool results from history to free context.',
|
|
22
|
-
'COST-AWARE: ~300-token round-trip; only call if freed tokens clearly exceed it — i.e. MULTIPLE bulky results (e.g. a wide grep/read sweep of mostly-irrelevant hits), not a single small one or near done.',
|
|
23
|
-
'Never dropped: system prompt and the CURRENT turn (last user message onward). Idempotent. Filters AND-combine; omit both = drop all droppable. Returns dropped count, freed tokens, tool names.',
|
|
24
|
-
].join(' '),
|
|
25
|
-
parameters: {
|
|
26
|
-
type: 'object',
|
|
27
|
-
properties: {
|
|
28
|
-
toolNames: {
|
|
29
|
-
type: 'array',
|
|
30
|
-
items: { type: 'string' },
|
|
31
|
-
description: 'Only drop results from these tool names (e.g. ["grep","read_file"]). Empty/omitted = no tool-name filter.',
|
|
32
|
-
},
|
|
33
|
-
contains: {
|
|
34
|
-
type: 'array',
|
|
35
|
-
items: { type: 'string' },
|
|
36
|
-
description: 'Only drop results whose content contains ALL of these keywords (AND, case-insensitive). Empty/omitted = no content filter.',
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
required: [],
|
|
40
|
-
},
|
|
41
|
-
async execute(args, ctx) {
|
|
42
|
-
const dropContext = ctx?.dropContext;
|
|
43
|
-
if (!dropContext) {
|
|
44
|
-
// 无注入(理论上不会:runAgentCore 总注入)。降级:不改 history,告知 agent。
|
|
45
|
-
return '错误:上下文剔除回调不可用(未由 agent 循环注入),无法剔除。';
|
|
46
|
-
}
|
|
47
|
-
const filter = {};
|
|
48
|
-
if (Array.isArray(args.toolNames)) {
|
|
49
|
-
filter.toolNames = args.toolNames
|
|
50
|
-
.filter((v) => typeof v === 'string' && v.length > 0)
|
|
51
|
-
.map((v) => String(v));
|
|
52
|
-
}
|
|
53
|
-
if (Array.isArray(args.contains)) {
|
|
54
|
-
filter.contains = args.contains
|
|
55
|
-
.filter((v) => typeof v === 'string' && v.length > 0)
|
|
56
|
-
.map((v) => String(v));
|
|
57
|
-
}
|
|
58
|
-
const result = dropContext(filter);
|
|
59
|
-
return result.dropped === 0
|
|
60
|
-
? '未剔除任何工具结果(无匹配的旧 tool 消息,或均在当前轮保护区内不可剔除)。'
|
|
61
|
-
: [
|
|
62
|
-
`已剔除 ${result.dropped} 条无关工具结果,释放约 ${result.freedTokens} tokens。`,
|
|
63
|
-
'被剔除项(已替换为存根,tool_call_id 配对不变):',
|
|
64
|
-
...result.items.map((it) => ` - ${it.toolName} (id …${it.toolCallId})`),
|
|
65
|
-
'这些结果在后续上下文中仅保留存根标记,不再占用篇幅。',
|
|
66
|
-
].join('\n');
|
|
67
|
-
},
|
|
68
|
-
};
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'node:module';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
-
const SUPPORTED_EXTENSIONS = new Set([
|
|
6
|
-
'.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs',
|
|
7
|
-
]);
|
|
8
|
-
async function loadTypeScript(root) {
|
|
9
|
-
const candidates = [];
|
|
10
|
-
try {
|
|
11
|
-
candidates.push(createRequire(path.join(root, 'package.json')).resolve('typescript'));
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
// Target project may not depend on TypeScript; fall back to mocode's installation.
|
|
15
|
-
}
|
|
16
|
-
try {
|
|
17
|
-
candidates.push(createRequire(import.meta.url).resolve('typescript'));
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
// A production install may intentionally omit the optional parser.
|
|
21
|
-
}
|
|
22
|
-
for (const candidate of [...new Set(candidates)]) {
|
|
23
|
-
try {
|
|
24
|
-
const loaded = await import(pathToFileURL(candidate).href);
|
|
25
|
-
return loaded.default ?? loaded;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// Try the next resolution root.
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
function severity(category, ts) {
|
|
34
|
-
if (category === ts.DiagnosticCategory.Error)
|
|
35
|
-
return 'error';
|
|
36
|
-
if (category === ts.DiagnosticCategory.Warning)
|
|
37
|
-
return 'warning';
|
|
38
|
-
return 'info';
|
|
39
|
-
}
|
|
40
|
-
/** Parse only changed TS/JS files; package-wide semantic checking remains V3. */
|
|
41
|
-
export async function runChangedFileDiagnostics(root, changedFiles, inputFingerprint) {
|
|
42
|
-
const startedAt = Date.now();
|
|
43
|
-
const files = [...new Set(changedFiles)]
|
|
44
|
-
.map((file) => path.resolve(process.cwd(), file))
|
|
45
|
-
.filter((file) => SUPPORTED_EXTENSIONS.has(path.extname(file).toLowerCase()));
|
|
46
|
-
if (files.length === 0) {
|
|
47
|
-
return {
|
|
48
|
-
level: 'V1', status: 'skipped', adapter: 'typescript-parser', diagnostics: [],
|
|
49
|
-
output: 'No changed TypeScript or JavaScript files.', durationMs: Date.now() - startedAt,
|
|
50
|
-
skipReason: 'unsupported_files', inputFingerprint,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
const ts = await loadTypeScript(root);
|
|
54
|
-
if (!ts) {
|
|
55
|
-
return {
|
|
56
|
-
level: 'V1', status: 'skipped', adapter: 'typescript-parser', diagnostics: [],
|
|
57
|
-
output: 'TypeScript parser is unavailable.', durationMs: Date.now() - startedAt,
|
|
58
|
-
skipReason: 'typescript_unavailable', inputFingerprint,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
const diagnostics = [];
|
|
62
|
-
for (const file of files) {
|
|
63
|
-
let source;
|
|
64
|
-
try {
|
|
65
|
-
source = await readFile(file, 'utf8');
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
diagnostics.push({
|
|
69
|
-
level: 'V1', source: 'typescript', severity: 'error', code: 'READ_FAILED',
|
|
70
|
-
file: path.relative(root, file), message: error instanceof Error ? error.message : String(error),
|
|
71
|
-
});
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const result = ts.transpileModule(source, {
|
|
75
|
-
fileName: file,
|
|
76
|
-
reportDiagnostics: true,
|
|
77
|
-
compilerOptions: {
|
|
78
|
-
allowJs: true,
|
|
79
|
-
jsx: ts.JsxEmit.Preserve,
|
|
80
|
-
module: ts.ModuleKind.ESNext,
|
|
81
|
-
target: ts.ScriptTarget.Latest,
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
for (const item of result.diagnostics ?? []) {
|
|
85
|
-
const location = item.file && item.start !== undefined
|
|
86
|
-
? item.file.getLineAndCharacterOfPosition(item.start)
|
|
87
|
-
: undefined;
|
|
88
|
-
diagnostics.push({
|
|
89
|
-
level: 'V1',
|
|
90
|
-
source: 'typescript',
|
|
91
|
-
severity: severity(item.category, ts),
|
|
92
|
-
code: item.code,
|
|
93
|
-
file: item.file ? path.relative(root, item.file.fileName) : path.relative(root, file),
|
|
94
|
-
line: location ? location.line + 1 : undefined,
|
|
95
|
-
column: location ? location.character + 1 : undefined,
|
|
96
|
-
message: ts.flattenDiagnosticMessageText(item.messageText, '\n'),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const failed = diagnostics.some((item) => item.severity === 'error');
|
|
101
|
-
const output = diagnostics.length === 0
|
|
102
|
-
? `Parsed ${files.length} changed TypeScript/JavaScript file(s).`
|
|
103
|
-
: diagnostics.map((item) => `${item.file ?? '<unknown>'}:${item.line ?? 0}:${item.column ?? 0} TS${item.code ?? ''} ${item.message}`).join('\n');
|
|
104
|
-
return {
|
|
105
|
-
level: 'V1', status: failed ? 'failed' : 'passed', adapter: 'typescript-parser',
|
|
106
|
-
diagnostics, output, durationMs: Date.now() - startedAt, inputFingerprint,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
function sha256(value) {
|
|
5
|
-
return createHash('sha256').update(value).digest('hex');
|
|
6
|
-
}
|
|
7
|
-
function normalizeText(value, root) {
|
|
8
|
-
const normalizedRoot = path.resolve(root).replace(/\\/g, '/');
|
|
9
|
-
return value
|
|
10
|
-
.replace(/\u001b\[[0-9;]*m/g, '')
|
|
11
|
-
.replace(/\\/g, '/')
|
|
12
|
-
.replaceAll(normalizedRoot, '<root>')
|
|
13
|
-
.replace(/\r\n?/g, '\n')
|
|
14
|
-
.trim();
|
|
15
|
-
}
|
|
16
|
-
/** Fingerprint the relevant on-disk inputs, so rewriting identical content can reuse validation. */
|
|
17
|
-
export function fingerprintFiles(root, files) {
|
|
18
|
-
const entries = [...new Set(files)].sort().map((file) => {
|
|
19
|
-
const absolute = path.resolve(process.cwd(), file);
|
|
20
|
-
const display = path.relative(root, absolute).replace(/\\/g, '/');
|
|
21
|
-
try {
|
|
22
|
-
const stat = statSync(absolute);
|
|
23
|
-
if (!stat.isFile())
|
|
24
|
-
return `${display}\0<${stat.isDirectory() ? 'directory' : 'other'}>`;
|
|
25
|
-
return `${display}\0${sha256(readFileSync(absolute))}`;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return `${display}\0<missing>`;
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
return sha256(entries.join('\n'));
|
|
32
|
-
}
|
|
33
|
-
export function fingerprintValidation(input) {
|
|
34
|
-
const diagnostics = [...input.diagnostics]
|
|
35
|
-
.map((item) => ({
|
|
36
|
-
source: item.source,
|
|
37
|
-
severity: item.severity,
|
|
38
|
-
code: item.code ?? '',
|
|
39
|
-
file: item.file ? normalizeText(item.file, input.root) : '',
|
|
40
|
-
line: item.line ?? 0,
|
|
41
|
-
column: item.column ?? 0,
|
|
42
|
-
message: normalizeText(item.message, input.root),
|
|
43
|
-
packageName: item.packageName ?? '',
|
|
44
|
-
}))
|
|
45
|
-
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
46
|
-
return sha256(JSON.stringify({
|
|
47
|
-
level: input.level ?? '',
|
|
48
|
-
status: input.status,
|
|
49
|
-
adapter: input.adapter ?? '',
|
|
50
|
-
command: input.command ? normalizeText(input.command, input.root) : '',
|
|
51
|
-
diagnostics,
|
|
52
|
-
output: diagnostics.length === 0 ? normalizeText(input.output ?? '', input.root) : '',
|
|
53
|
-
}));
|
|
54
|
-
}
|