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,93 +1,129 @@
|
|
|
1
|
-
// Relevance Pruner:
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
// Relevance Pruner: statically removes tool results that a newer observation supersedes.
|
|
2
|
+
// It never deletes messages or changes tool_call_id pairing; only tool content is stubbed.
|
|
3
|
+
import { canonicalizePath, extractPath, isToolResultSuccess, lastUserIndex, toText, } from './utils.js';
|
|
4
|
+
/** Shared prefix lets /context count read and observation supersession together. */
|
|
5
|
+
const STUB_PREFIX = '⌦[已过时:';
|
|
6
|
+
const READ_STUB_REASON = '同 path 已有新 read / 已被 mutation 覆写';
|
|
7
|
+
function parseArgs(raw) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
10
|
+
return parsed && typeof parsed === 'object'
|
|
11
|
+
? parsed
|
|
12
|
+
: null;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function normalizedInteger(value, fallback) {
|
|
19
|
+
const n = Number(value);
|
|
20
|
+
return Number.isFinite(n) ? Math.trunc(n) : fallback;
|
|
21
|
+
}
|
|
22
|
+
/** Only complete semantic-query equality is safe for whole-message replacement. */
|
|
23
|
+
function observationKey(call) {
|
|
24
|
+
const args = call.args;
|
|
25
|
+
if (!args)
|
|
26
|
+
return null;
|
|
27
|
+
if (call.name === 'grep') {
|
|
28
|
+
if (typeof args.pattern !== 'string')
|
|
29
|
+
return null;
|
|
30
|
+
const rawMax = normalizedInteger(args.max_per_file, 15);
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
tool: 'grep',
|
|
33
|
+
pattern: args.pattern,
|
|
34
|
+
glob: typeof args.glob === 'string' ? args.glob : '**/*',
|
|
35
|
+
maxPerFile: Math.min(Math.max(rawMax, 1), 50),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (call.name === 'codegraph') {
|
|
39
|
+
if (typeof args.action !== 'string' || typeof args.query !== 'string')
|
|
40
|
+
return null;
|
|
41
|
+
const query = args.action === 'explore'
|
|
42
|
+
? args.query.trim().replace(/\s+/g, ' ')
|
|
43
|
+
: args.query.trim();
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
tool: 'codegraph',
|
|
46
|
+
action: args.action,
|
|
47
|
+
query,
|
|
48
|
+
file: typeof args.file === 'string' ? args.file.trim().replace(/\\/g, '/') : '',
|
|
49
|
+
offset: args.offset === undefined ? null : normalizedInteger(args.offset, 0),
|
|
50
|
+
limit: args.limit === undefined ? null : normalizedInteger(args.limit, 0),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function observationLabel(call) {
|
|
56
|
+
const args = call.args ?? {};
|
|
57
|
+
if (call.name === 'grep') {
|
|
58
|
+
const pattern = JSON.stringify(String(args.pattern ?? '')).slice(0, 80);
|
|
59
|
+
const glob = JSON.stringify(String(args.glob ?? '**/*')).slice(0, 80);
|
|
60
|
+
return `grep(pattern=${pattern}, glob=${glob})`;
|
|
61
|
+
}
|
|
62
|
+
const action = String(args.action ?? '');
|
|
63
|
+
const query = JSON.stringify(String(args.query ?? '')).slice(0, 100);
|
|
64
|
+
return `codegraph(action=${action}, query=${query})`;
|
|
65
|
+
}
|
|
29
66
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* 设计:每个 agent 会话(每个 runAgentCore 实例)持有一个 pruner。会话结束/换 plan 时
|
|
35
|
-
* 可新建;不持久化(history 重建时索引自然过期)。
|
|
36
|
-
*
|
|
37
|
-
* 零依赖:仅依赖 ChatMessage 形状;不 import llm / tools / agent。
|
|
67
|
+
* Cross-message relevance pruning:
|
|
68
|
+
* - read_file: a newer successful read of the same canonical path supersedes old reads.
|
|
69
|
+
* - grep/codegraph: a newer successful call with the exact same semantic arguments
|
|
70
|
+
* supersedes old results. Partial file overlap is intentionally not enough.
|
|
38
71
|
*/
|
|
39
72
|
export class RelevancePruner {
|
|
40
|
-
/** path → [history index, ...] 按插入序;最新在末尾。 */
|
|
41
73
|
readByPath = new Map();
|
|
42
|
-
|
|
43
|
-
* - 只处理成功的 read_file tool 消息;失败读取不能淘汰旧的有效结果。
|
|
44
|
-
* - 登记当前 canonical path,并反向 stub 同 path 旧 read。
|
|
45
|
-
*/
|
|
74
|
+
observationByKey = new Map();
|
|
46
75
|
observePush(history, msg, succeeded = true) {
|
|
47
76
|
try {
|
|
48
77
|
if (!succeeded || msg.role !== 'tool')
|
|
49
78
|
return;
|
|
50
|
-
const m = msg;
|
|
51
79
|
const idx = history.length - 1;
|
|
52
80
|
if (idx < 1 || history[idx] !== msg)
|
|
53
|
-
return; // 防御:必须刚 push 到末尾
|
|
54
|
-
if (toolNameOf(history, idx) !== 'read_file')
|
|
55
81
|
return;
|
|
56
82
|
const content = toText(msg.content);
|
|
57
83
|
if (content.startsWith(STUB_PREFIX))
|
|
58
|
-
return;
|
|
59
|
-
const
|
|
60
|
-
if (!
|
|
61
|
-
return;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
return;
|
|
85
|
+
const call = this.callAt(history, idx);
|
|
86
|
+
if (!call)
|
|
87
|
+
return;
|
|
88
|
+
if (call.name === 'read_file') {
|
|
89
|
+
const path = canonicalizePath(extractPath(call.argsRaw));
|
|
90
|
+
if (!path)
|
|
91
|
+
return;
|
|
92
|
+
this.stubPriorReads(history, path, idx);
|
|
93
|
+
const list = this.readByPath.get(path) ?? [];
|
|
66
94
|
list.push(idx);
|
|
67
|
-
|
|
68
|
-
|
|
95
|
+
this.readByPath.set(path, list);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const key = observationKey(call);
|
|
99
|
+
if (!key)
|
|
100
|
+
return;
|
|
101
|
+
this.stubPriorObservations(history, call.name, key, idx);
|
|
102
|
+
const list = this.observationByKey.get(key) ?? [];
|
|
103
|
+
list.push(idx);
|
|
104
|
+
this.observationByKey.set(key, list);
|
|
69
105
|
}
|
|
70
106
|
catch {
|
|
71
|
-
|
|
107
|
+
// Relevance pruning must never block tool-result insertion.
|
|
72
108
|
}
|
|
73
109
|
}
|
|
74
|
-
|
|
75
|
-
pathAt(history, idx) {
|
|
110
|
+
callAt(history, idx) {
|
|
76
111
|
const tcId = history[idx]?.tool_call_id;
|
|
77
112
|
if (!tcId)
|
|
78
113
|
return null;
|
|
79
114
|
for (let j = idx - 1; j >= 1; j--) {
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
115
|
+
const message = history[j];
|
|
116
|
+
if (message.role !== 'assistant')
|
|
117
|
+
continue;
|
|
118
|
+
const calls = message.tool_calls;
|
|
119
|
+
const hit = calls?.find((tc) => tc?.id === tcId);
|
|
120
|
+
if (!hit?.function?.name)
|
|
82
121
|
continue;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
if (hit)
|
|
86
|
-
return canonicalizePath(extractPath(hit.function?.arguments));
|
|
122
|
+
const argsRaw = hit.function.arguments ?? '';
|
|
123
|
+
return { name: hit.function.name, argsRaw, args: parseArgs(argsRaw) };
|
|
87
124
|
}
|
|
88
125
|
return null;
|
|
89
126
|
}
|
|
90
|
-
/** 成功 mutation 后,把该 canonical path 在 mutation 之前的 read 全部 stub。 */
|
|
91
127
|
observeMutation(history, path) {
|
|
92
128
|
try {
|
|
93
129
|
const canonicalPath = canonicalizePath(path);
|
|
@@ -100,93 +136,102 @@ export class RelevancePruner {
|
|
|
100
136
|
this.readByPath.delete(canonicalPath);
|
|
101
137
|
}
|
|
102
138
|
catch {
|
|
103
|
-
|
|
139
|
+
// Never throw from mutation cleanup.
|
|
104
140
|
}
|
|
105
141
|
}
|
|
106
|
-
/**
|
|
107
|
-
* 把 history 里 "canonical path 同 + index < beforeIdx + 不在当前轮保护区" 的 read_file
|
|
108
|
-
* tool 消息替换为存根。索引是快路径,全表扫描用于恢复 resume 历史;两条路径都重新校验 path。
|
|
109
|
-
*/
|
|
110
142
|
stubPriorReads(history, path, beforeIdx) {
|
|
111
143
|
const targetPath = canonicalizePath(path);
|
|
112
144
|
if (!targetPath)
|
|
113
145
|
return;
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
if (i >= beforeIdx)
|
|
146
|
+
const protectedFrom = Math.max(0, lastUserIndex(history));
|
|
147
|
+
const stubOne = (idx) => {
|
|
148
|
+
if (idx >= beforeIdx || (protectedFrom > 0 && idx >= protectedFrom))
|
|
118
149
|
return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const m = history[i];
|
|
122
|
-
if (!m || m.role !== 'tool')
|
|
150
|
+
const message = history[idx];
|
|
151
|
+
if (!message || message.role !== 'tool')
|
|
123
152
|
return;
|
|
124
|
-
const content = toText(
|
|
153
|
+
const content = toText(message.content);
|
|
125
154
|
if (content.startsWith(STUB_PREFIX))
|
|
126
|
-
return; // 幂等
|
|
127
|
-
if (toolNameOf(history, i) !== 'read_file')
|
|
128
155
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
156
|
+
const call = this.callAt(history, idx);
|
|
157
|
+
if (call?.name !== 'read_file')
|
|
158
|
+
return;
|
|
159
|
+
if (canonicalizePath(extractPath(call.argsRaw)) !== targetPath)
|
|
160
|
+
return;
|
|
161
|
+
if (!message.tool_call_id)
|
|
133
162
|
return;
|
|
134
|
-
|
|
135
|
-
|
|
163
|
+
message.content =
|
|
164
|
+
`${STUB_PREFIX}${READ_STUB_REASON}] read_file(${targetPath}) ${content.length} 字符 ` +
|
|
165
|
+
`→ 已被新 read / mutation 替代 · id …${message.tool_call_id.slice(-6)}⌫`;
|
|
136
166
|
};
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
for (const i of indexed)
|
|
140
|
-
stubOne(i);
|
|
141
|
-
}
|
|
167
|
+
for (const idx of this.readByPath.get(targetPath) ?? [])
|
|
168
|
+
stubOne(idx);
|
|
142
169
|
const scanEnd = Math.min(beforeIdx, protectedFrom > 0 ? protectedFrom : beforeIdx);
|
|
143
|
-
for (let
|
|
144
|
-
stubOne(
|
|
170
|
+
for (let idx = 1; idx < scanEnd; idx++)
|
|
171
|
+
stubOne(idx);
|
|
172
|
+
}
|
|
173
|
+
stubPriorObservations(history, toolName, key, beforeIdx) {
|
|
174
|
+
const protectedFrom = Math.max(0, lastUserIndex(history));
|
|
175
|
+
const stubOne = (idx) => {
|
|
176
|
+
if (idx >= beforeIdx || (protectedFrom > 0 && idx >= protectedFrom))
|
|
177
|
+
return;
|
|
178
|
+
const message = history[idx];
|
|
179
|
+
if (!message || message.role !== 'tool')
|
|
180
|
+
return;
|
|
181
|
+
const content = toText(message.content);
|
|
182
|
+
if (content.startsWith(STUB_PREFIX) || !isToolResultSuccess(content))
|
|
183
|
+
return;
|
|
184
|
+
const call = this.callAt(history, idx);
|
|
185
|
+
if (!call || call.name !== toolName || observationKey(call) !== key)
|
|
186
|
+
return;
|
|
187
|
+
if (!message.tool_call_id)
|
|
188
|
+
return;
|
|
189
|
+
const reason = toolName === 'grep'
|
|
190
|
+
? '相同 grep 查询已有更新结果'
|
|
191
|
+
: '相同 codegraph 查询已有更新结果';
|
|
192
|
+
message.content =
|
|
193
|
+
`${STUB_PREFIX}${reason}] ${observationLabel(call)} ${content.length} 字符 ` +
|
|
194
|
+
`→ 已被更新查询替代 · id …${message.tool_call_id.slice(-6)}⌫`;
|
|
195
|
+
};
|
|
196
|
+
for (const idx of this.observationByKey.get(key) ?? [])
|
|
197
|
+
stubOne(idx);
|
|
198
|
+
// The fallback scan restores correctness after resume/compact when this instance
|
|
199
|
+
// has no index for older messages.
|
|
200
|
+
const scanEnd = Math.min(beforeIdx, protectedFrom > 0 ? protectedFrom : beforeIdx);
|
|
201
|
+
for (let idx = 1; idx < scanEnd; idx++)
|
|
202
|
+
stubOne(idx);
|
|
145
203
|
}
|
|
146
204
|
}
|
|
147
|
-
/** 默认单例:每个 agent 循环一个。runAgentCore 入口 new 一个,后续 observe 共享。 */
|
|
148
205
|
export function createRelevancePruner() {
|
|
149
206
|
return new RelevancePruner();
|
|
150
207
|
}
|
|
151
|
-
/**
|
|
208
|
+
/** Parse the original content length recorded by any relevance stub. */
|
|
152
209
|
function parseStubOriginalLen(stub) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (!m)
|
|
210
|
+
const match = /\) (\d+) 字符 →/.exec(stub);
|
|
211
|
+
if (!match)
|
|
156
212
|
return null;
|
|
157
|
-
const
|
|
158
|
-
return Number.isFinite(
|
|
213
|
+
const value = Number(match[1]);
|
|
214
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
159
215
|
}
|
|
160
|
-
/**
|
|
161
|
-
* 扫 history,统计被相关性裁剪 stub 的 read_file tool 消息(条数 + 原字节数)。
|
|
162
|
-
* 供 /context 渲染统计行用(让用户直观看到「prune 帮了多少」)。
|
|
163
|
-
* 永不抛错(对齐本模块契约);history 为空 / 无 stub 时返零值。
|
|
164
|
-
*
|
|
165
|
-
* 注意:stub 后只剩 stub 字符串(原 content 已丢失),故只能从 stub 字符串里 parse
|
|
166
|
-
* 原字节数,误差 = stub 时记录的 content.length(精确);token 估算走 estimateTokens。
|
|
167
|
-
*/
|
|
216
|
+
/** Aggregate relevance/lifecycle compression for the /context panel. */
|
|
168
217
|
export function computePruneStats(history) {
|
|
169
218
|
let stubbed = 0;
|
|
170
219
|
let originalChars = 0;
|
|
171
220
|
let stubChars = 0;
|
|
172
|
-
for (const
|
|
173
|
-
if (
|
|
221
|
+
for (const message of history) {
|
|
222
|
+
if (message.role !== 'tool')
|
|
174
223
|
continue;
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const isDigest = c.startsWith('⌦[摘要:');
|
|
224
|
+
const content = toText(message.content);
|
|
225
|
+
const isPruneStub = content.startsWith(STUB_PREFIX);
|
|
226
|
+
const isDigest = content.startsWith('⌦[摘要:');
|
|
179
227
|
if (!isPruneStub && !isDigest)
|
|
180
228
|
continue;
|
|
181
229
|
stubbed++;
|
|
182
|
-
stubChars +=
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
originalChars +=
|
|
230
|
+
stubChars += content.length;
|
|
231
|
+
const original = parseStubOriginalLen(content);
|
|
232
|
+
if (original != null)
|
|
233
|
+
originalChars += original;
|
|
186
234
|
}
|
|
187
|
-
// token 估算:用 estimateTokens(懒导入,避免循环依赖 llm)
|
|
188
|
-
// 这里偷懒:走粗略 chars/4(中文混合下会过估,安全侧)
|
|
189
|
-
// 准确应调 estimateTokens,但 /context 已经是粗算,误差可接受
|
|
190
235
|
const originalTokens = Math.ceil(originalChars / 4);
|
|
191
236
|
const stubTokens = Math.ceil(stubChars / 4);
|
|
192
237
|
return {
|
package/dist/session/compact.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Spinner } from '../ui/spinner.js';
|
|
|
6
6
|
import * as layout from '../ui/layout.js';
|
|
7
7
|
import { pruneAfterCompaction } from '../rollback/index.js';
|
|
8
8
|
import { toText } from '../context/utils.js';
|
|
9
|
+
import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
|
|
9
10
|
export function createContextState() {
|
|
10
11
|
return { lastEstimate: 0, correction: 1, calibrationSamples: 0 };
|
|
11
12
|
}
|
|
@@ -241,8 +242,8 @@ export async function compactHistory(history, opts) {
|
|
|
241
242
|
const estimateBefore = estimatePromptTokens(history, activeTools, state.correction);
|
|
242
243
|
state.lastEstimate = estimateBefore;
|
|
243
244
|
const groups = groupFromEnd(history);
|
|
244
|
-
//
|
|
245
|
-
const keepBudget = Math.floor(opts.window *
|
|
245
|
+
// 保近期:按策略中的 token 比例累积(至少保 1 组),永不劈开 group。
|
|
246
|
+
const keepBudget = Math.floor(opts.window * DEFAULT_BUDGET_POLICY.compactKeepRatio);
|
|
246
247
|
const kept = [];
|
|
247
248
|
let keptTokens = 0;
|
|
248
249
|
for (let k = groups.length - 1; k >= 0; k--) {
|
package/dist/session/index.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, contextState, createContextState, } from './compact.js';
|
|
8
8
|
// ── Context Budget Scheduler 接缝 ────────────────────────────────────────
|
|
9
|
-
// agent/core.ts
|
|
10
|
-
//
|
|
11
|
-
// repl /compact 命令调 manualCompact(history, focus?)
|
|
9
|
+
// agent/core.ts 在 age-aware sweep 后调用 runScheduler(history, step):评估五区预算,
|
|
10
|
+
// 只执行可落地的 warn / compact_history;开关关闭时退化为 maybeCompact。
|
|
11
|
+
// repl /compact 命令调 manualCompact(history, focus?):与自动路径共享决策,focus 透传摘要 prompt。
|
|
12
12
|
export { runScheduler, manualCompact, createBudgetScheduler, } from './scheduler.js';
|
|
13
13
|
export { dropContextFromHistory, formatDropResult, } from './drop.js';
|
|
14
14
|
export { newSessionId, saveSession, loadSession, listSessions, sessionDir, } from './persist.js';
|
|
@@ -1,30 +1,17 @@
|
|
|
1
|
-
// Context Budget Scheduler(执行层)
|
|
2
|
-
//
|
|
3
|
-
// 关系图:
|
|
1
|
+
// Context Budget Scheduler(执行层):评估五区预算并执行可落地的动作。
|
|
4
2
|
//
|
|
5
3
|
// agent/core.ts (步前) repl /compact 命令(手动)
|
|
6
4
|
// ↓ runScheduler(history, step) ↓ manualCompact(history, focus?)
|
|
7
5
|
// session/scheduler.ts(本文件)
|
|
8
6
|
// ├─ evaluateBudget(history, window, step) → BudgetReport
|
|
9
|
-
//
|
|
10
|
-
// ├─ scheduleActions(report) → ScheduleAction[]
|
|
11
|
-
// │ ↓
|
|
7
|
+
// ├─ scheduleActions(report) → warn | compact_history
|
|
12
8
|
// └─ 执行 actions:
|
|
13
|
-
// - warn:
|
|
14
|
-
// -
|
|
15
|
-
// - shrink_cold_tools L2: 由 pruner.observePush(relevance.ts)自动处理;此处 no-op
|
|
16
|
-
// - shrink_cold_tools L3: 由 lifecycle.pushTool(lifecycle.ts)自动处理;此处 no-op
|
|
17
|
-
// - cap_hot_tools: Hot 区只 cap,实际仍由 push-time cap 走;此处 no-op + 记日志
|
|
18
|
-
// - compact_history: 调 maybeCompact(history, report)── report 路由到 ROI 调度
|
|
9
|
+
// - warn: 仅写日志
|
|
10
|
+
// - compact_history: 调 maybeCompact(history, report)
|
|
19
11
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// - **hotBoundary** 仍由调度器算出来供 lifecycle 内部用(将来可演进成「仅 Cold 区跑 age stub」)——
|
|
24
|
-
// 现版本先全面暴露给 report,暂不传参给 lifecycle。
|
|
25
|
-
// - **actionLog**:每次执行的决策落进 contextState.schedulerLog,供 /context 命令与调试用。
|
|
26
|
-
// - **manualCompact**:手动入口(用户敲 /compact),与 runScheduler 共享决策路径;唯一差别是
|
|
27
|
-
// 即使 history 不超预算也强制产 compact_history(focus 透传)。对齐用户拍板的方案 A。
|
|
12
|
+
// push-time cap / relevance / lifecycle / age-aware sweep 在进入 scheduler 前独立完成,
|
|
13
|
+
// scheduler 不再生成无法执行的 Cold/Hot tool action。每次决策写入 actionLog,供 /context 调试。
|
|
14
|
+
// manualCompact 与自动路径共享 scheduleActions,但用户显式触发时强制追加 compact_history。
|
|
28
15
|
//
|
|
29
16
|
// 开关:
|
|
30
17
|
// - config.contextBudget !== false(默认 true):agent 调 runScheduler
|
|
@@ -36,15 +23,10 @@ import { config } from '../config/index.js';
|
|
|
36
23
|
import { maybeCompact, contextState } from './compact.js';
|
|
37
24
|
import * as layout from '../ui/layout.js';
|
|
38
25
|
import { ui } from '../ui/theme.js';
|
|
39
|
-
/** 创建 runAgentCore 闭包持有的 scheduler(每次 agent 启动一个新实例)。
|
|
40
|
-
* observePush 当前只是占位:真正 L1/L2/L3 已由 cap / pruner / lifecycle 在 push 时跑;
|
|
41
|
-
* 保留接口为后续「调度器注入 hotBoundary 给 lifecycle」演进留接缝。 */
|
|
26
|
+
/** 创建 runAgentCore 闭包持有的 scheduler(每次 agent 启动一个新实例)。 */
|
|
42
27
|
export function createBudgetScheduler(state = contextState) {
|
|
43
28
|
const obs = {
|
|
44
29
|
lastRunLog: null,
|
|
45
|
-
observePush(_history, _idx) {
|
|
46
|
-
// 占位:push-time 三闸(cap / pruner / lifecycle)已自动跑;此接缝供将来演进。
|
|
47
|
-
},
|
|
48
30
|
async runStep(history, step, activeTools = chatTools) {
|
|
49
31
|
const report = evaluateBudget(history, config.contextWindowTokens, step, state.correction, activeTools);
|
|
50
32
|
const actions = scheduleActions(report);
|
|
@@ -53,7 +35,7 @@ export function createBudgetScheduler(state = contextState) {
|
|
|
53
35
|
for (const a of actions) {
|
|
54
36
|
if (a.kind === 'warn') {
|
|
55
37
|
// system 超:写一行提示(配置漂移应由用户处理,不是调度器压)
|
|
56
|
-
layout.contentWrite(` ${ui.yellow}●${ui.reset} ${ui.yellow}
|
|
38
|
+
layout.contentWrite(` ${ui.yellow}●${ui.reset} ${ui.yellow}调度器警告 [${a.layer}] ${a.reason}${ui.reset}\n`);
|
|
57
39
|
}
|
|
58
40
|
else if (a.kind === 'compact_history') {
|
|
59
41
|
// 路由到 maybeCompact;把结构重建信号传回 core,使 lifecycle 按新 index 恢复。
|
|
@@ -61,9 +43,6 @@ export function createBudgetScheduler(state = contextState) {
|
|
|
61
43
|
compactHistoryCalled = true;
|
|
62
44
|
historyRebuilt ||= result?.historyRebuilt === true;
|
|
63
45
|
}
|
|
64
|
-
// shrink_cold_tools L1/L2/L3 与 cap_hot_tools:已由 push-time 闸在每次 push 自动跑
|
|
65
|
-
// (cap = MAX_HISTORY_RESULT;pruner = same-path 新旧替换;lifecycle = age stub)。
|
|
66
|
-
// 调度器不重复,只把决策记录下来供调试。
|
|
67
46
|
}
|
|
68
47
|
const log = {
|
|
69
48
|
step,
|
|
@@ -85,9 +64,9 @@ export async function runScheduler(history, step, state = contextState, activeTo
|
|
|
85
64
|
const s = createBudgetScheduler(state);
|
|
86
65
|
return s.runStep(history, step, activeTools);
|
|
87
66
|
}
|
|
88
|
-
/** 手动 /compact 入口(repl)
|
|
89
|
-
* 即便 layers.history.overBudget=false 或 totalOver=false
|
|
90
|
-
*
|
|
67
|
+
/** 手动 /compact 入口(repl):与自动路径共享预算评估和可执行 action,但强制执行 history 摘要。
|
|
68
|
+
* 即便 layers.history.overBudget=false 或 totalOver=false,manual 仍追加 compact_history,
|
|
69
|
+
* 并把 focus 透传给 LLM 摘要 prompt。
|
|
91
70
|
*
|
|
92
71
|
* 关系:runScheduler 是「自动触发」,manualCompact 是「用户显式触发」,二者共享 scheduleActions。
|
|
93
72
|
*
|
package/dist/tools/constants.js
CHANGED
|
@@ -23,23 +23,6 @@ export const GC_DAYS = 90;
|
|
|
23
23
|
/** memory_search 结果(召回的记忆正文)的放宽上限:指令性内容,中截破坏语义,对齐 use_skill。 */
|
|
24
24
|
export const MAX_MEMORY_RESULT = 64000;
|
|
25
25
|
export const IGNORE = ['**/node_modules/**', '**/.git/**'];
|
|
26
|
-
// ── Context Budget Scheduler(五区分账)────────────────────────────────────
|
|
27
|
-
/** Hot/Cold 划分:当前 step 起往前 N 个 user turn 之内的工具结果视为 Hot(绝对不压),
|
|
28
|
-
* 之外的视为 Cold(可调度器压)。默认 4 = 跨过 4 个用户问题仍生效。 */
|
|
29
|
-
export const HOT_TURN_WINDOW = 4;
|
|
30
|
-
/** Cold 区内可被就地 stub 的 tool 消息最低 age(经过的消费者 push 数)。默认 2,
|
|
31
|
-
* 与 lifecycle.ts DEFAULT_AGE_THRESHOLD 对齐。 */
|
|
32
|
-
export const TOOL_OLD_AGE = 2;
|
|
33
|
-
/** 五区预算占比(总和 0.95,留 5% 给 Reserve)。对齐 user 修正版:
|
|
34
|
-
* System 15 / History 20 / Hot Tool 25 / Cold Tool 25 / Summary 10。 */
|
|
35
|
-
export const BUDGET_RATIO = {
|
|
36
|
-
system: 0.15,
|
|
37
|
-
history: 0.20,
|
|
38
|
-
toolRecent: 0.25,
|
|
39
|
-
toolOld: 0.25,
|
|
40
|
-
summary: 0.10,
|
|
41
|
-
reserve: 0.05,
|
|
42
|
-
};
|
|
43
26
|
// ── plan 模式(只读规划,不执行)──────────────────────────────────────────────
|
|
44
27
|
/**
|
|
45
28
|
* plan 模式下从工具 schema 里剔除的工具(模型根本看不到 → 调不到):
|