mocode-ai 0.6.6 → 0.6.7
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/dist/agent/core.js +35 -15
- package/dist/context/age-aware.js +136 -0
- package/dist/context/budget.js +49 -74
- package/dist/context/encoders/code.js +186 -60
- package/dist/context/encoders/graph.js +69 -19
- package/dist/context/encoders/search.js +116 -50
- package/dist/context/index.js +1 -1
- package/dist/context/pipeline.js +5 -1
- package/dist/context/relevance.js +166 -121
- package/dist/session/compact.js +3 -2
- package/dist/session/index.js +3 -3
- package/dist/session/scheduler.js +12 -33
- package/dist/tools/constants.js +0 -17
- package/package.json +1 -1
|
@@ -1,76 +1,202 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Code Encoder(read_file):折叠连续空行(≥3 → 1,保留首空行行号前缀)。
|
|
3
|
-
*
|
|
4
|
-
* 输入:read_file 返回的 ` N\t<content>` 行(6 宽右对齐行号 + tab + 内容),可能带尾部
|
|
5
|
-
* `... (N 行未显示,共 M 行)`。
|
|
6
|
-
* 输出:连续 ≥3 个空行(行号前缀 + 空 content)折叠为 1 个(保留首行前缀,丢后续空行);其余逐字保留。
|
|
7
|
-
*
|
|
8
|
-
* 不变量(离线脚本断言):
|
|
9
|
-
* - 所有非空 content 行逐字保留(前缀 + 内容不变)→ edit_file 的 old_string 按内容匹配仍可用。
|
|
10
|
-
* - content 行的行号前缀不变 → LLM 看到的行号与文件一致。
|
|
11
|
-
* - 仅空行(前缀 + tab + 空 content)被折叠,且仅 ≥3 连续时;≤2 空行原样(常见,不动)。
|
|
12
|
-
* - 尾部标记 `... (...)` 不匹配行号前缀,原样保留。
|
|
13
|
-
*
|
|
14
|
-
* ⚠️ 残余风险(可接受、可恢复):若 LLM 编辑一个含 ≥3 连续空行的区域,它看到的空行数比文件实际少,
|
|
15
|
-
* old_string 的空行数可能不匹配 → edit_file 返"未找到" → LLM 重读重试(系统提示已要求编辑前 read_file)。
|
|
16
|
-
* 真实代码极少 ≥3 连续空行(linter 通常强制 ≤2),故实际几乎不触发。出问题可设
|
|
17
|
-
* MOCODE_CONTEXT_OPTIMIZE=false 全局回退,或调高阈值。
|
|
18
|
-
*
|
|
19
|
-
* 不做长度裁剪(cap 兜底);不动行号前缀;不删 content 行;不改内容(含尾随空白)。
|
|
20
|
-
* 空行判定用前缀感知(前缀 + tab + 空 content),不能用 trim——read_file 空行 ` 2\t` trim 后剩 `2`。
|
|
21
|
-
*/
|
|
1
|
+
/** read_file line: right-aligned source line number + tab + exact content. */
|
|
22
2
|
const LINE_RE = /^(\s*\d+)\t(.*)$/;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
3
|
+
const JS_LIKE_PATH_RE = /\.(?:[cm]?js|jsx|ts|tsx)$/i;
|
|
4
|
+
function parseLine(raw) {
|
|
5
|
+
const match = LINE_RE.exec(raw);
|
|
6
|
+
if (!match)
|
|
7
|
+
return null;
|
|
8
|
+
return { raw, line: Number(match[1].trim()), content: match[2] };
|
|
26
9
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (run >= 3) {
|
|
41
|
-
out.push(lines[i]); // 保留首空行(含其行号前缀)
|
|
42
|
-
collapsedRuns++;
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
for (let k = 0; k < run; k++)
|
|
46
|
-
out.push(lines[i]);
|
|
47
|
-
}
|
|
48
|
-
i = j;
|
|
10
|
+
function collapseBlankCodeRuns(output) {
|
|
11
|
+
const lines = output.split('\n');
|
|
12
|
+
const out = [];
|
|
13
|
+
let collapsed = 0;
|
|
14
|
+
let i = 0;
|
|
15
|
+
while (i < lines.length) {
|
|
16
|
+
if (parseLine(lines[i])?.content === '') {
|
|
17
|
+
let j = i;
|
|
18
|
+
while (j < lines.length && parseLine(lines[j])?.content === '')
|
|
19
|
+
j++;
|
|
20
|
+
if (j - i >= 3) {
|
|
21
|
+
out.push(lines[i]);
|
|
22
|
+
collapsed++;
|
|
49
23
|
}
|
|
50
24
|
else {
|
|
51
|
-
|
|
25
|
+
for (let k = i; k < j; k++)
|
|
26
|
+
out.push(lines[k]);
|
|
27
|
+
}
|
|
28
|
+
i = j;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
out.push(lines[i++]);
|
|
32
|
+
}
|
|
33
|
+
return { text: out.join('\n'), collapsed };
|
|
34
|
+
}
|
|
35
|
+
function isJsLikePath(args) {
|
|
36
|
+
return typeof args?.path === 'string' && JS_LIKE_PATH_RE.test(args.path);
|
|
37
|
+
}
|
|
38
|
+
function isSingleLineImport(content) {
|
|
39
|
+
const line = content.trim();
|
|
40
|
+
return /^import\b/.test(line) && /(?:['"][^'"]+['"]\s*;?|;)$/.test(line);
|
|
41
|
+
}
|
|
42
|
+
function collapseImportBlocks(text) {
|
|
43
|
+
const lines = text.split('\n');
|
|
44
|
+
const out = [];
|
|
45
|
+
let collapsed = 0;
|
|
46
|
+
let i = 0;
|
|
47
|
+
while (i < lines.length) {
|
|
48
|
+
const first = parseLine(lines[i]);
|
|
49
|
+
if (!first || !isSingleLineImport(first.content)) {
|
|
50
|
+
out.push(lines[i++]);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
let j = i + 1;
|
|
54
|
+
while (j < lines.length) {
|
|
55
|
+
const next = parseLine(lines[j]);
|
|
56
|
+
if (!next || !isSingleLineImport(next.content))
|
|
57
|
+
break;
|
|
58
|
+
j++;
|
|
59
|
+
}
|
|
60
|
+
const run = j - i;
|
|
61
|
+
if (run >= 4) {
|
|
62
|
+
const last = parseLine(lines[j - 1]);
|
|
63
|
+
out.push(lines[i]);
|
|
64
|
+
out.push(`… ${run - 2} import lines folded (source lines ${first.line + 1}–${last.line - 1}; cold read)`);
|
|
65
|
+
out.push(lines[j - 1]);
|
|
66
|
+
collapsed++;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
for (let k = i; k < j; k++)
|
|
70
|
+
out.push(lines[k]);
|
|
71
|
+
}
|
|
72
|
+
i = j;
|
|
73
|
+
}
|
|
74
|
+
const result = out.join('\n');
|
|
75
|
+
return result.length < text.length
|
|
76
|
+
? { text: result, collapsed }
|
|
77
|
+
: { text, collapsed: 0 };
|
|
78
|
+
}
|
|
79
|
+
function braceDelta(line, state) {
|
|
80
|
+
let delta = 0;
|
|
81
|
+
for (let i = 0; i < line.length; i++) {
|
|
82
|
+
const ch = line[i];
|
|
83
|
+
const next = line[i + 1];
|
|
84
|
+
if (state.blockComment) {
|
|
85
|
+
if (ch === '*' && next === '/') {
|
|
86
|
+
state.blockComment = false;
|
|
52
87
|
i++;
|
|
53
88
|
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (state.quote) {
|
|
92
|
+
if (state.escaped) {
|
|
93
|
+
state.escaped = false;
|
|
94
|
+
}
|
|
95
|
+
else if (ch === '\\') {
|
|
96
|
+
state.escaped = true;
|
|
97
|
+
}
|
|
98
|
+
else if (ch === state.quote) {
|
|
99
|
+
state.quote = null;
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === '/' && next === '/')
|
|
104
|
+
break;
|
|
105
|
+
if (ch === '/' && next === '*') {
|
|
106
|
+
state.blockComment = true;
|
|
107
|
+
i++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
111
|
+
state.quote = ch;
|
|
112
|
+
state.escaped = false;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (ch === '{')
|
|
116
|
+
delta++;
|
|
117
|
+
else if (ch === '}')
|
|
118
|
+
delta--;
|
|
119
|
+
}
|
|
120
|
+
state.escaped = false;
|
|
121
|
+
return delta;
|
|
122
|
+
}
|
|
123
|
+
function isFunctionStart(content) {
|
|
124
|
+
return (/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function(?:\s+[\w$]+)?\s*\(/.test(content) ||
|
|
125
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+[\w$]+\s*=.*=>\s*\{\s*$/.test(content) ||
|
|
126
|
+
/^\s*(?:(?:public|private|protected|static|abstract|override|async|get|set)\s+)*(?:constructor|[\w$]+)\s*\([^;]*\)\s*(?::[^={]+)?\s*\{\s*$/.test(content));
|
|
127
|
+
}
|
|
128
|
+
function collapseFunctionBodies(text) {
|
|
129
|
+
const lines = text.split('\n');
|
|
130
|
+
const out = [];
|
|
131
|
+
let collapsed = 0;
|
|
132
|
+
let i = 0;
|
|
133
|
+
while (i < lines.length) {
|
|
134
|
+
const start = parseLine(lines[i]);
|
|
135
|
+
if (!start || !isFunctionStart(start.content)) {
|
|
136
|
+
out.push(lines[i++]);
|
|
137
|
+
continue;
|
|
54
138
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
139
|
+
const state = { quote: null, escaped: false, blockComment: false };
|
|
140
|
+
let depth = braceDelta(start.content, state);
|
|
141
|
+
if (depth <= 0) {
|
|
142
|
+
out.push(lines[i++]);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
let j = i + 1;
|
|
146
|
+
for (; j < lines.length && depth > 0; j++) {
|
|
147
|
+
const current = parseLine(lines[j]);
|
|
148
|
+
if (!current)
|
|
149
|
+
break;
|
|
150
|
+
depth += braceDelta(current.content, state);
|
|
151
|
+
}
|
|
152
|
+
const endIndex = j - 1;
|
|
153
|
+
const end = parseLine(lines[endIndex] ?? '');
|
|
154
|
+
const bodyLines = endIndex - i - 1;
|
|
155
|
+
if (depth === 0 && end && bodyLines >= 8) {
|
|
156
|
+
out.push(lines[i]);
|
|
157
|
+
out.push(`… ${bodyLines} function-body lines folded (source lines ${start.line + 1}–${end.line - 1}; cold read)`);
|
|
158
|
+
out.push(lines[endIndex]);
|
|
159
|
+
collapsed++;
|
|
160
|
+
i = j;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
out.push(lines[i++]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const result = out.join('\n');
|
|
167
|
+
return result.length < text.length
|
|
168
|
+
? { text: result, collapsed }
|
|
169
|
+
: { text, collapsed: 0 };
|
|
170
|
+
}
|
|
171
|
+
export const codeEncoder = {
|
|
172
|
+
kind: 'code',
|
|
173
|
+
encode(input) {
|
|
174
|
+
const blankResult = collapseBlankCodeRuns(input.output);
|
|
175
|
+
let text = blankResult.text;
|
|
176
|
+
const notes = [];
|
|
177
|
+
if (blankResult.collapsed)
|
|
178
|
+
notes.push(`collapsed ${blankResult.collapsed} blank runs`);
|
|
179
|
+
if (input.phase === 'sweep' &&
|
|
180
|
+
input.isCold === true &&
|
|
181
|
+
(input.age ?? 0) >= 2 &&
|
|
182
|
+
input.isFirstRead === false &&
|
|
183
|
+
isJsLikePath(input.args)) {
|
|
184
|
+
const imports = collapseImportBlocks(text);
|
|
185
|
+
text = imports.text;
|
|
186
|
+
if (imports.collapsed)
|
|
187
|
+
notes.push(`folded ${imports.collapsed} import blocks`);
|
|
188
|
+
const functions = collapseFunctionBodies(text);
|
|
189
|
+
text = functions.text;
|
|
190
|
+
if (functions.collapsed)
|
|
191
|
+
notes.push(`folded ${functions.collapsed} function bodies`);
|
|
65
192
|
}
|
|
66
|
-
const text = out.join('\n');
|
|
67
193
|
return {
|
|
68
194
|
text,
|
|
69
195
|
meta: {
|
|
70
196
|
kind: 'code',
|
|
71
|
-
originalLen: output.length,
|
|
197
|
+
originalLen: input.output.length,
|
|
72
198
|
encodedLen: text.length,
|
|
73
|
-
note:
|
|
199
|
+
note: notes.join(', ') || 'no change',
|
|
74
200
|
},
|
|
75
201
|
};
|
|
76
202
|
},
|
|
@@ -1,30 +1,80 @@
|
|
|
1
1
|
import { stripAnsi, collapseBlankRuns } from './_util.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
function isAgedCold(input) {
|
|
3
|
+
return input.phase === 'sweep' && input.isCold === true && (input.age ?? 0) >= 2;
|
|
4
|
+
}
|
|
5
|
+
function normalizeBlock(block) {
|
|
6
|
+
return block
|
|
7
|
+
.replace(/\r\n/g, '\n')
|
|
8
|
+
.split('\n')
|
|
9
|
+
.map((line) => line.trimEnd())
|
|
10
|
+
.join('\n')
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
function dedupeFencedBlocks(text) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
let removed = 0;
|
|
16
|
+
const result = text.replace(/```[^\n]*\n[\s\S]*?\n```/g, (block) => {
|
|
17
|
+
const key = normalizeBlock(block);
|
|
18
|
+
if (key.length < 120 || !seen.has(key)) {
|
|
19
|
+
seen.add(key);
|
|
20
|
+
return block;
|
|
21
|
+
}
|
|
22
|
+
removed++;
|
|
23
|
+
return '… duplicate source block omitted (cold graph)';
|
|
24
|
+
});
|
|
25
|
+
return { text: result, removed };
|
|
26
|
+
}
|
|
27
|
+
function looksLikeSourceBlock(block) {
|
|
28
|
+
const lines = block.split('\n');
|
|
29
|
+
const sourceLines = lines.filter((line) => /^\s*\d+\t/.test(line) ||
|
|
30
|
+
/^\s*(?:L)?\d+[:|]\s/.test(line) ||
|
|
31
|
+
/^.*:\d+(?::\d+)?:\s/.test(line)).length;
|
|
32
|
+
return block.length >= 120 && sourceLines >= 3;
|
|
33
|
+
}
|
|
34
|
+
function dedupeParagraphSourceBlocks(text) {
|
|
35
|
+
const parts = text.split(/(\n{2,})/);
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
let removed = 0;
|
|
38
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
39
|
+
const block = parts[i];
|
|
40
|
+
if (!looksLikeSourceBlock(block))
|
|
41
|
+
continue;
|
|
42
|
+
const key = normalizeBlock(block);
|
|
43
|
+
if (seen.has(key)) {
|
|
44
|
+
parts[i] = '… duplicate source block omitted (cold graph)';
|
|
45
|
+
removed++;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
seen.add(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { text: parts.join(''), removed };
|
|
52
|
+
}
|
|
11
53
|
export const graphEncoder = {
|
|
12
54
|
kind: 'graph',
|
|
13
|
-
encode(
|
|
14
|
-
const stripped = stripAnsi(output);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
55
|
+
encode(input) {
|
|
56
|
+
const stripped = stripAnsi(input.output);
|
|
57
|
+
let text = stripped;
|
|
58
|
+
let duplicates = 0;
|
|
59
|
+
if (isAgedCold(input)) {
|
|
60
|
+
const fenced = dedupeFencedBlocks(text);
|
|
61
|
+
const paragraphs = dedupeParagraphSourceBlocks(fenced.text);
|
|
62
|
+
text = paragraphs.text;
|
|
63
|
+
duplicates = fenced.removed + paragraphs.removed;
|
|
64
|
+
}
|
|
65
|
+
text = collapseBlankRuns(text, 3);
|
|
66
|
+
const notes = [
|
|
67
|
+
/\x1b/.test(input.output) ? 'ANSI stripped' : '',
|
|
68
|
+
text !== stripped && duplicates === 0 ? 'blank runs collapsed' : '',
|
|
69
|
+
duplicates > 0 ? `${duplicates} duplicate source blocks omitted` : '',
|
|
70
|
+
].filter(Boolean);
|
|
21
71
|
return {
|
|
22
72
|
text,
|
|
23
73
|
meta: {
|
|
24
74
|
kind: 'graph',
|
|
25
|
-
originalLen: output.length,
|
|
75
|
+
originalLen: input.output.length,
|
|
26
76
|
encodedLen: text.length,
|
|
27
|
-
note,
|
|
77
|
+
note: notes.join(', ') || 'no change',
|
|
28
78
|
},
|
|
29
79
|
};
|
|
30
80
|
},
|
|
@@ -1,68 +1,134 @@
|
|
|
1
|
-
const
|
|
1
|
+
const LEGACY_GREP_RE = /^(.*?):(\d+):(.*)$/;
|
|
2
|
+
const STRUCTURED_HEADER_RE = /^(.*): (\d+) 处匹配,行号 \[([0-9,\s]+)\]$/;
|
|
3
|
+
const STRUCTURED_BODY_RE = /^\s{2}L\d+:/;
|
|
4
|
+
const STRUCTURED_FOLDED_RE = /^\s{2}\(body 已折叠/;
|
|
5
|
+
function isAgedCold(input) {
|
|
6
|
+
return input.phase === 'sweep' && input.isCold === true && (input.age ?? 0) >= 2;
|
|
7
|
+
}
|
|
8
|
+
/** Current grep output already has lossless file headers; Cold drops body previews only. */
|
|
9
|
+
function collapseStructuredGrep(output) {
|
|
10
|
+
const lines = output.split('\n');
|
|
11
|
+
const out = [];
|
|
12
|
+
let files = 0;
|
|
13
|
+
let inFile = false;
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (STRUCTURED_HEADER_RE.test(line)) {
|
|
16
|
+
files++;
|
|
17
|
+
inFile = true;
|
|
18
|
+
out.push(line);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (inFile && (STRUCTURED_BODY_RE.test(line) || STRUCTURED_FOLDED_RE.test(line))) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
inFile = false;
|
|
25
|
+
out.push(line);
|
|
26
|
+
}
|
|
27
|
+
return files > 0 ? { text: out.join('\n'), files } : null;
|
|
28
|
+
}
|
|
29
|
+
function encodeLegacyGrep(output) {
|
|
30
|
+
const lines = output.split('\n');
|
|
31
|
+
const matches = [];
|
|
32
|
+
const tail = [];
|
|
33
|
+
let inTail = false;
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
if (!line)
|
|
36
|
+
continue;
|
|
37
|
+
if (inTail) {
|
|
38
|
+
tail.push(line);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const match = LEGACY_GREP_RE.exec(line);
|
|
42
|
+
if (match) {
|
|
43
|
+
matches.push({ file: match[1], line: match[2], content: match[3] });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
inTail = true;
|
|
47
|
+
tail.push(line);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (matches.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
const groups = new Map();
|
|
53
|
+
const order = [];
|
|
54
|
+
for (const match of matches) {
|
|
55
|
+
if (!groups.has(match.file)) {
|
|
56
|
+
groups.set(match.file, []);
|
|
57
|
+
order.push(match.file);
|
|
58
|
+
}
|
|
59
|
+
groups.get(match.file).push({ line: match.line, content: match.content });
|
|
60
|
+
}
|
|
61
|
+
const out = [`# ${matches.length} matches · ${order.length} files · search-encoded`];
|
|
62
|
+
for (const file of order) {
|
|
63
|
+
out.push(`${file}:`);
|
|
64
|
+
for (const item of groups.get(file))
|
|
65
|
+
out.push(` ${item.line}:${item.content}`);
|
|
66
|
+
}
|
|
67
|
+
if (tail.length)
|
|
68
|
+
out.push(...tail);
|
|
69
|
+
return { text: out.join('\n'), matches: matches.length, files: order.length };
|
|
70
|
+
}
|
|
71
|
+
function collapseLegacyBodies(text) {
|
|
72
|
+
if (!text.startsWith('# ') || !text.includes('search-encoded'))
|
|
73
|
+
return text;
|
|
74
|
+
return text
|
|
75
|
+
.split('\n')
|
|
76
|
+
.filter((line) => !/^\s{2}\d+:/.test(line))
|
|
77
|
+
.join('\n');
|
|
78
|
+
}
|
|
2
79
|
export const searchEncoder = {
|
|
3
80
|
kind: 'search',
|
|
4
|
-
encode(
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
let inTail = false;
|
|
9
|
-
for (const l of lines) {
|
|
10
|
-
if (!l)
|
|
11
|
-
continue;
|
|
12
|
-
if (inTail) {
|
|
13
|
-
tail.push(l);
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
const m = GREP_RE.exec(l);
|
|
17
|
-
if (m) {
|
|
18
|
-
matches.push({ file: m[1], line: m[2], content: m[3] });
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
// 首个非 grep 行起视作 tail(grep 上限标记 / 无匹配串 / web_search 非 grep 结构)
|
|
22
|
-
inTail = true;
|
|
23
|
-
tail.push(l);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if (matches.length === 0) {
|
|
27
|
-
// 非 grep 格式(web_search 等)→ 不动其已格式化结构
|
|
81
|
+
encode(input) {
|
|
82
|
+
const structured = collapseStructuredGrep(input.output);
|
|
83
|
+
if (structured) {
|
|
84
|
+
const text = isAgedCold(input) ? structured.text : input.output;
|
|
28
85
|
return {
|
|
29
|
-
text
|
|
86
|
+
text,
|
|
30
87
|
meta: {
|
|
31
88
|
kind: 'search',
|
|
32
|
-
originalLen: output.length,
|
|
33
|
-
encodedLen:
|
|
34
|
-
note:
|
|
89
|
+
originalLen: input.output.length,
|
|
90
|
+
encodedLen: text.length,
|
|
91
|
+
note: isAgedCold(input)
|
|
92
|
+
? `${structured.files} files · Cold body previews removed`
|
|
93
|
+
: `${structured.files} structured grep files · passthrough`,
|
|
35
94
|
},
|
|
36
95
|
};
|
|
37
96
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
97
|
+
// A previous pass may already have transformed legacy file:line output.
|
|
98
|
+
if (input.output.startsWith('# ') && input.output.includes('search-encoded')) {
|
|
99
|
+
const text = isAgedCold(input) ? collapseLegacyBodies(input.output) : input.output;
|
|
100
|
+
return {
|
|
101
|
+
text,
|
|
102
|
+
meta: {
|
|
103
|
+
kind: 'search',
|
|
104
|
+
originalLen: input.output.length,
|
|
105
|
+
encodedLen: text.length,
|
|
106
|
+
note: isAgedCold(input) ? 'Cold legacy bodies removed' : 'already encoded',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
46
109
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
110
|
+
const legacy = encodeLegacyGrep(input.output);
|
|
111
|
+
if (!legacy) {
|
|
112
|
+
return {
|
|
113
|
+
text: input.output,
|
|
114
|
+
meta: {
|
|
115
|
+
kind: 'search',
|
|
116
|
+
originalLen: input.output.length,
|
|
117
|
+
encodedLen: input.output.length,
|
|
118
|
+
note: 'no recognized grep structure → passthrough',
|
|
119
|
+
},
|
|
120
|
+
};
|
|
55
121
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
122
|
+
const text = isAgedCold(input)
|
|
123
|
+
? collapseLegacyBodies(legacy.text)
|
|
124
|
+
: legacy.text;
|
|
59
125
|
return {
|
|
60
126
|
text,
|
|
61
127
|
meta: {
|
|
62
128
|
kind: 'search',
|
|
63
|
-
originalLen: output.length,
|
|
129
|
+
originalLen: input.output.length,
|
|
64
130
|
encodedLen: text.length,
|
|
65
|
-
note: `${matches
|
|
131
|
+
note: `${legacy.matches} matches / ${legacy.files} files${isAgedCold(input) ? ' · Cold bodies removed' : ''}`,
|
|
66
132
|
},
|
|
67
133
|
};
|
|
68
134
|
},
|
package/dist/context/index.js
CHANGED
|
@@ -8,4 +8,4 @@ export { optimizeToolResult } from './pipeline.js';
|
|
|
8
8
|
export { classify, knownToolKinds } from './classifier.js';
|
|
9
9
|
export { registerEncoder, registerAll, getEncoder, registeredKinds, } from './registry.js';
|
|
10
10
|
// ── Context Budget Scheduler ───────────────────────────────────────────────
|
|
11
|
-
export { evaluateBudget, scheduleActions, formatReport, quickEstimate, userTurnBoundary, BUDGET_LAYERS, BUDGET_RATIO, HOT_TURN_WINDOW, TOOL_OLD_AGE, } from './budget.js';
|
|
11
|
+
export { evaluateBudget, scheduleActions, formatReport, quickEstimate, userTurnBoundary, BUDGET_LAYERS, DEFAULT_BUDGET_POLICY, BUDGET_RATIO, HOT_TURN_WINDOW, TOOL_OLD_AGE, } from './budget.js';
|
package/dist/context/pipeline.js
CHANGED
|
@@ -58,7 +58,7 @@ function budgetFor(name) {
|
|
|
58
58
|
* @param argsRaw 工具 arguments 原始 JSON 字符串(tc.arguments,可空;未传则 args=null)
|
|
59
59
|
* @returns 进 history 的 content 字符串(永不抛错)
|
|
60
60
|
*/
|
|
61
|
-
export function optimizeToolResult(name, output, argsRaw) {
|
|
61
|
+
export function optimizeToolResult(name, output, argsRaw, context = {}) {
|
|
62
62
|
boot();
|
|
63
63
|
// 总开关关闭:完全走老路径,零行为变化(Phase 1 默认 true,但保留紧急回退开关)。
|
|
64
64
|
if (!config.contextOptimize) {
|
|
@@ -73,6 +73,10 @@ export function optimizeToolResult(name, output, argsRaw) {
|
|
|
73
73
|
output,
|
|
74
74
|
args,
|
|
75
75
|
budget: budgetFor(name),
|
|
76
|
+
age: context.age ?? 0,
|
|
77
|
+
isCold: context.isCold ?? false,
|
|
78
|
+
isFirstRead: context.isFirstRead,
|
|
79
|
+
phase: context.phase ?? 'push',
|
|
76
80
|
});
|
|
77
81
|
// 末尾长度裁剪兜底(同改造前):encoder 已更短则 no-op;use_skill/memory_search 的放宽 cap 由此保留。
|
|
78
82
|
return capToolResultForHistory(name, text);
|