mocode-ai 1.2.6 → 1.2.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 +72 -32
- package/dist/agent/index.js +70 -34
- package/dist/agent/spawn.js +4 -0
- package/dist/commands/skill.js +230 -0
- package/dist/config/index.js +66 -4
- package/dist/config/presets.js +29 -7
- package/dist/context/artifacts.js +20 -0
- package/dist/context/budget.js +15 -6
- package/dist/context/index.js +1 -1
- package/dist/host/stdio.js +10 -1
- package/dist/llm/index.js +22 -2
- package/dist/llm/providers/anthropic.js +370 -0
- package/dist/repl/index.js +85 -40
- package/dist/session/compact.js +24 -2
- package/dist/session/persist.js +2 -2
- package/dist/session/scheduler.js +4 -4
- package/dist/skills/skill-eval.js +345 -0
- package/dist/skills/skill-improve.js +221 -0
- package/dist/skills/stats.js +102 -0
- package/dist/tools/registry.js +5 -1
- package/dist/ui/layout.js +11 -0
- package/dist/verification/prompt.js +55 -0
- package/package.json +2 -2
package/dist/config/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import dotenv from 'dotenv';
|
|
|
5
5
|
import { getCurrentSessionId } from '../session/state.js';
|
|
6
6
|
import { getNotesFilePath, extractActiveNotesSections } from '../session/notes.js';
|
|
7
7
|
import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
|
|
8
|
+
import { buildValidationCommandsSection } from '../verification/prompt.js';
|
|
8
9
|
import { detectLanguage, setLanguage, t, } from '../i18n/index.js';
|
|
9
10
|
/**
|
|
10
11
|
* 按优先级加载配置文件并回填 process.env:
|
|
@@ -40,11 +41,14 @@ export const languageFromShell = process.env.MOCODE_LANGUAGE !== undefined;
|
|
|
40
41
|
// 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
|
|
41
42
|
// 仿 themeFromShell 模式:shell export 的环境变量在 loadEnvFiles 中不被回填(优先级最高),
|
|
42
43
|
// 故 /model 写入 ~/.mocode/config 的同名键下次启动会被 shell 值覆盖——据此给 dim 警告。
|
|
43
|
-
const LLM_ENV_KEYS = ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS'];
|
|
44
|
+
const LLM_ENV_KEYS = ['LLM_PROVIDER', 'LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS', 'ANTHROPIC_PROMPT_CACHE'];
|
|
44
45
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 256000;
|
|
45
46
|
const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
|
|
46
47
|
loadEnvFiles();
|
|
47
48
|
setLanguage(detectLanguage(process.env.MOCODE_LANGUAGE));
|
|
49
|
+
export function normalizeLlmProvider(value) {
|
|
50
|
+
return typeof value === 'string' && value.toLowerCase() === 'anthropic' ? 'anthropic' : 'openai';
|
|
51
|
+
}
|
|
48
52
|
/**
|
|
49
53
|
* 取环境变量;缺则返回空字符串(不退出)。
|
|
50
54
|
* 历史上缺 LLM_BASE_URL/LLM_API_KEY 会 process.exit(1),但 /model 命令已能在 REPL 内配置模型,
|
|
@@ -243,15 +247,43 @@ export function reinjectSessionNotesIntoSystem(history) {
|
|
|
243
247
|
return true;
|
|
244
248
|
}
|
|
245
249
|
/**
|
|
246
|
-
* 一次性重注入会话状态(plan 段 + 笔记段)
|
|
247
|
-
*
|
|
248
|
-
*
|
|
250
|
+
* 一次性重注入会话状态(plan 段 + 笔记段)到系统提示。返回任一 marker 是否改动。
|
|
251
|
+
*
|
|
252
|
+
* @deprecated 热路径已不再调用(#prompt-cache):往 history[0] 追加 plan/笔记会让
|
|
253
|
+
* 系统提示每次 plan_update / note_append 后变字节,前缀缓存整段失效(系统提示 6-8k token,
|
|
254
|
+
* 本轮后续每步全价重算)。现由 agent/core 每步在 requestHistory **末尾**注入
|
|
255
|
+
* {@link buildSessionStateReminder} 的 ephemeral system 消息:模型看到的信息等价,
|
|
256
|
+
* 但变动落在前缀末端。本函数仅留给外部集成 / 旧测试,新增调用点请勿使用。
|
|
249
257
|
*/
|
|
250
258
|
export function reinjectSessionStateIntoSystem(history) {
|
|
251
259
|
const planChanged = reinjectActivePlanIntoSystem(history);
|
|
252
260
|
const notesChanged = reinjectSessionNotesIntoSystem(history);
|
|
253
261
|
return planChanged || notesChanged;
|
|
254
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* 构造"会话状态提醒"正文(活跃 `## Plan:` 段 + 活跃笔记段正文),供 agent/core 每步
|
|
265
|
+
* 拼进 requestHistory **末尾**的 ephemeral system 消息。
|
|
266
|
+
*
|
|
267
|
+
* 为什么在尾部而不是 history[0](prompt 缓存):plan_update / note_append 是设计上鼓励
|
|
268
|
+
* 高频调用的工具,一旦它们改写系统提示,支持自动前缀缓存的后端(OpenAI / DeepSeek /
|
|
269
|
+
* GLM / Qwen)就会从第一个 token 起全部 miss。放到历史末尾后,前面整段(系统提示 + 全部
|
|
270
|
+
* 已有对话)保持逐字节稳定,只有尾部这一小条随 notes.md 变化。
|
|
271
|
+
*
|
|
272
|
+
* 纯读函数:不改 history,也不写文件。notes.md 不存在 / 无活跃内容时返回 ''(零开销)。
|
|
273
|
+
*/
|
|
274
|
+
export function buildSessionStateReminder(sessionId = getCurrentSessionId()) {
|
|
275
|
+
const plan = extractActivePlanSection(sessionId);
|
|
276
|
+
const notes = extractActiveNotesSections(undefined, sessionId);
|
|
277
|
+
if (!plan && !notes)
|
|
278
|
+
return '';
|
|
279
|
+
const parts = [
|
|
280
|
+
'## Session state (current, from notes.md)',
|
|
281
|
+
'This block mirrors the live session notepad and is refreshed every step; treat it as the authoritative plan/notes state, and ignore any older copy earlier in this conversation.',
|
|
282
|
+
...(plan ? [plan] : []),
|
|
283
|
+
...(notes ? [notes] : []),
|
|
284
|
+
];
|
|
285
|
+
return parts.join('\n\n');
|
|
286
|
+
}
|
|
255
287
|
const SYSTEM_PROMPT_MEMORY_SECTION = `
|
|
256
288
|
## Memory (cross-session facts)
|
|
257
289
|
- The prompt may contain a title/summary index; retrieve details with memory_search or inspect all with memory_list. memory_search also surfaces knowledge-graph facts (relations between entities) alongside entry bodies.
|
|
@@ -345,6 +377,7 @@ Complete programming tasks through an "analyze → call tool → observe result
|
|
|
345
377
|
- Report: stop when done and give honest conclusions with path:line references (see Reporting).
|
|
346
378
|
- Use web search only when freshness materially affects the answer.
|
|
347
379
|
${buildCodegraphSection()}
|
|
380
|
+
${buildValidationCommandsSection()}
|
|
348
381
|
|
|
349
382
|
## Engineering principles
|
|
350
383
|
${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
@@ -446,6 +479,7 @@ export function getPlanModeSuffix() {
|
|
|
446
479
|
return buildPlanModeSuffix();
|
|
447
480
|
}
|
|
448
481
|
export const config = {
|
|
482
|
+
provider: normalizeLlmProvider(process.env.LLM_PROVIDER),
|
|
449
483
|
baseURL: requireEnv('LLM_BASE_URL'),
|
|
450
484
|
apiKey: requireEnv('LLM_API_KEY'),
|
|
451
485
|
model: process.env.LLM_MODEL || 'gpt-4o-mini',
|
|
@@ -457,6 +491,7 @@ export const config = {
|
|
|
457
491
|
},
|
|
458
492
|
contextWindowTokens: Number(process.env.CONTEXT_WINDOW_TOKENS) || DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
459
493
|
includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
|
|
494
|
+
anthropicPromptCache: process.env.ANTHROPIC_PROMPT_CACHE !== 'false',
|
|
460
495
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
461
496
|
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE === 'true',
|
|
462
497
|
contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE === 'true',
|
|
@@ -482,6 +517,21 @@ export const config = {
|
|
|
482
517
|
permissionEnabled: process.env.MOCODE_PERMISSION !== 'false',
|
|
483
518
|
permissionNonInteractiveAllow: process.env.MOCODE_PERMISSION_NON_INTERACTIVE_ALLOW === 'true',
|
|
484
519
|
};
|
|
520
|
+
/**
|
|
521
|
+
* 会话钉死模型:窗口/会话启动时由 pinSessionModel() 捕获一次。
|
|
522
|
+
* 运行中 agent 一律经 getActiveModel() 取模型,而非热切的 config.model——
|
|
523
|
+
* 这样某窗口 /model switch 改写全局 config 后,其它【已经打开】的窗口的
|
|
524
|
+
* 运行 agent 仍用各自启动时的模型,不会被影响;只有重启/新开窗口才会读全局 config。
|
|
525
|
+
*/
|
|
526
|
+
let sessionModel = null;
|
|
527
|
+
/** 在 REPL 启动时调用一次,把当前模型钉成本会话的活跃模型。 */
|
|
528
|
+
export function pinSessionModel() {
|
|
529
|
+
sessionModel = config.model;
|
|
530
|
+
}
|
|
531
|
+
/** 运行中 agent 实际使用的模型:优先钉死值,未钉(极早路径)则回退 config.model。 */
|
|
532
|
+
export function getActiveModel() {
|
|
533
|
+
return sessionModel ?? config.model;
|
|
534
|
+
}
|
|
485
535
|
/**
|
|
486
536
|
* 运行时更新模型相关配置(/model 命令调)。
|
|
487
537
|
* - 更新 config 对象字段(即时生效:chat() 读 config.model,reconfigureClient 读 config.baseURL/apiKey)。
|
|
@@ -491,8 +541,16 @@ export const config = {
|
|
|
491
541
|
* 重建 OpenAI 客户端(baseURL/apiKey 是构造时固化的实例字段)由调用方走 reconfigureClient。
|
|
492
542
|
*/
|
|
493
543
|
export function updateModelConfig(opts) {
|
|
544
|
+
if (opts.provider !== undefined) {
|
|
545
|
+
config.provider = opts.provider;
|
|
546
|
+
process.env.LLM_PROVIDER = opts.provider;
|
|
547
|
+
}
|
|
494
548
|
if (opts.model !== undefined) {
|
|
495
549
|
config.model = opts.model;
|
|
550
|
+
// 钉死值同步更新:本窗口显式 /model switch 立即对本窗口运行 agent 生效;
|
|
551
|
+
// 其它已开窗口的 sessionModel 不受影响(各自启动时钉死)。
|
|
552
|
+
if (sessionModel !== null)
|
|
553
|
+
sessionModel = opts.model;
|
|
496
554
|
process.env.LLM_MODEL = opts.model;
|
|
497
555
|
}
|
|
498
556
|
if (opts.baseURL !== undefined) {
|
|
@@ -507,6 +565,10 @@ export function updateModelConfig(opts) {
|
|
|
507
565
|
config.contextWindowTokens = opts.contextWindowTokens;
|
|
508
566
|
process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
|
|
509
567
|
}
|
|
568
|
+
if (opts.anthropicPromptCache !== undefined) {
|
|
569
|
+
config.anthropicPromptCache = opts.anthropicPromptCache;
|
|
570
|
+
process.env.ANTHROPIC_PROMPT_CACHE = opts.anthropicPromptCache ? 'true' : 'false';
|
|
571
|
+
}
|
|
510
572
|
}
|
|
511
573
|
/** 子 Agent 总开关;默认 false,关闭时 sub-agent 不进入模型工具表。 */
|
|
512
574
|
export function isSubAgentEnabled() {
|
package/dist/config/presets.js
CHANGED
|
@@ -31,8 +31,8 @@ function filePathFor(name) {
|
|
|
31
31
|
}
|
|
32
32
|
return path.join(MODELS_DIR, `${name}.json`);
|
|
33
33
|
}
|
|
34
|
-
/** 把磁盘上的 raw JSON 解析并校验为 ModelPreset
|
|
35
|
-
function parsePreset(raw) {
|
|
34
|
+
/** 把磁盘上的 raw JSON 解析并校验为 ModelPreset;旧预设缺 provider 时按 openai 读取。 */
|
|
35
|
+
export function parsePreset(raw) {
|
|
36
36
|
const obj = JSON.parse(raw);
|
|
37
37
|
const { name, baseURL, apiKey, model, contextWindow } = obj;
|
|
38
38
|
if (typeof name !== 'string' || !isValidPresetName(name)) {
|
|
@@ -50,7 +50,17 @@ function parsePreset(raw) {
|
|
|
50
50
|
if (typeof contextWindow !== 'number' || !Number.isFinite(contextWindow) || contextWindow <= 0) {
|
|
51
51
|
throw new Error(`预设 ${name}: contextWindow 必须为正数`);
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
const provider = obj.provider === 'anthropic' ? 'anthropic' : 'openai';
|
|
54
|
+
const anthropicPromptCache = provider === 'anthropic' && obj.anthropicPromptCache !== false;
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
provider,
|
|
58
|
+
baseURL,
|
|
59
|
+
apiKey,
|
|
60
|
+
model,
|
|
61
|
+
contextWindow: Math.floor(contextWindow),
|
|
62
|
+
anthropicPromptCache,
|
|
63
|
+
};
|
|
54
64
|
}
|
|
55
65
|
/** 读单个预设;不存在抛错。 */
|
|
56
66
|
export function getPreset(name) {
|
|
@@ -68,15 +78,21 @@ export function readPreset(name) {
|
|
|
68
78
|
throw e;
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
|
-
/** 写/覆盖一个预设(原子:写 tmp 再 rename)
|
|
81
|
+
/** 写/覆盖一个预设(原子:写 tmp 再 rename)。旧调用缺 provider 时仍按 openai 保存。 */
|
|
72
82
|
export function savePreset(preset) {
|
|
73
83
|
if (!isValidPresetName(preset.name)) {
|
|
74
84
|
throw new Error(`非法预设名: ${JSON.stringify(preset.name)}`);
|
|
75
85
|
}
|
|
86
|
+
const provider = preset.provider ?? 'openai';
|
|
87
|
+
const normalized = {
|
|
88
|
+
...preset,
|
|
89
|
+
provider,
|
|
90
|
+
anthropicPromptCache: provider === 'anthropic' && preset.anthropicPromptCache !== false,
|
|
91
|
+
};
|
|
76
92
|
fs.mkdirSync(MODELS_DIR, { recursive: true });
|
|
77
93
|
const dest = filePathFor(preset.name);
|
|
78
94
|
const tmp = `${dest}.tmp-${process.pid}-${Date.now()}`;
|
|
79
|
-
fs.writeFileSync(tmp, JSON.stringify(
|
|
95
|
+
fs.writeFileSync(tmp, JSON.stringify(normalized, null, 2), 'utf8');
|
|
80
96
|
fs.renameSync(tmp, dest);
|
|
81
97
|
}
|
|
82
98
|
/** 删除一个预设;不存在返回 false,成功返回 true。 */
|
|
@@ -156,11 +172,15 @@ export function migrateCurrentToPreset(input) {
|
|
|
156
172
|
return null;
|
|
157
173
|
if (!Number.isFinite(input.contextWindow) || input.contextWindow <= 0)
|
|
158
174
|
return null;
|
|
175
|
+
const provider = input.provider ?? 'openai';
|
|
176
|
+
const anthropicPromptCache = provider === 'anthropic' && input.anthropicPromptCache !== false;
|
|
159
177
|
const existing = listPresets();
|
|
160
|
-
const dup = existing.find((p) => p.
|
|
178
|
+
const dup = existing.find((p) => p.provider === provider &&
|
|
179
|
+
p.baseURL === input.baseURL &&
|
|
161
180
|
p.apiKey === input.apiKey &&
|
|
162
181
|
p.model === input.model &&
|
|
163
|
-
p.contextWindow === input.contextWindow
|
|
182
|
+
p.contextWindow === input.contextWindow &&
|
|
183
|
+
p.anthropicPromptCache === anthropicPromptCache);
|
|
164
184
|
if (dup)
|
|
165
185
|
return null;
|
|
166
186
|
// 'default' 已被占 → 用户已显式起过预设,无需老数据迁入;返回 null 让调用方跳过即可。
|
|
@@ -168,10 +188,12 @@ export function migrateCurrentToPreset(input) {
|
|
|
168
188
|
return null;
|
|
169
189
|
savePreset({
|
|
170
190
|
name: 'default',
|
|
191
|
+
provider,
|
|
171
192
|
baseURL: input.baseURL,
|
|
172
193
|
apiKey: input.apiKey,
|
|
173
194
|
model: input.model,
|
|
174
195
|
contextWindow: input.contextWindow,
|
|
196
|
+
anthropicPromptCache,
|
|
175
197
|
});
|
|
176
198
|
return 'default';
|
|
177
199
|
}
|
|
@@ -131,6 +131,26 @@ export function recordArtifact(state, history, idx, output, succeeded) {
|
|
|
131
131
|
stateFor(state).artifacts.set(id, artifact);
|
|
132
132
|
updateStats(state, stateFor(state));
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* 按读取时间倒序返回最近若干「仍新鲜的 read_file 目标」(path + hash)。
|
|
136
|
+
* 用途:文件编辑工具参数校验失败(如缺 path)时,把系统已知的候选直接回灌给模型照抄,
|
|
137
|
+
* 避免模型在长上下文里凭记忆复述出错、补一个字段丢另一个字段的乒乓重试。
|
|
138
|
+
* 只展示事实、不替模型填值——选哪个候选仍由模型判断。永不抛错。
|
|
139
|
+
*/
|
|
140
|
+
export function knownEditTargets(state, limit = 3) {
|
|
141
|
+
const artifacts = stateFor(state).artifacts;
|
|
142
|
+
const targets = [];
|
|
143
|
+
for (const artifact of artifacts.values()) {
|
|
144
|
+
if (artifact.freshness !== 'fresh' || artifact.source.type !== 'read')
|
|
145
|
+
continue;
|
|
146
|
+
const dependency = artifact.dependencies[0];
|
|
147
|
+
if (!dependency || dependency.path === '*' || !dependency.hash)
|
|
148
|
+
continue;
|
|
149
|
+
targets.push({ path: dependency.path, hash: dependency.hash, version: artifact.version ?? 0 });
|
|
150
|
+
}
|
|
151
|
+
targets.sort((a, b) => b.version - a.version);
|
|
152
|
+
return targets.slice(0, Math.max(1, Math.floor(limit) || 3)).map(({ path, hash }) => ({ path, hash }));
|
|
153
|
+
}
|
|
134
154
|
function affected(artifact, changed) {
|
|
135
155
|
// '*' 依赖(无法解析出具体文件路径的诊断/搜索结果)不与任何具体写操作关联:
|
|
136
156
|
// 任何文件写入都会作废全部 '*' artifact,等于每次 mutation 都销毁
|
package/dist/context/budget.js
CHANGED
|
@@ -54,8 +54,10 @@ export function userTurnBoundary(history, window) {
|
|
|
54
54
|
/** 评估当前 history 的五区预算(纯函数,改不动 history)。
|
|
55
55
|
* 传入 step 是当前所在 step 编号(agent 循环 step 变量),用于日志/调试。
|
|
56
56
|
* correction:API 实测 / 估算的校正系数(默认 1);>1 表示粗估偏低,乘以系数后 actual 更接近真实值。
|
|
57
|
-
* activeTools 必须与下一次 chat() 实际发送的工具集合一致,避免 plan/子 agent 误算 schema。
|
|
58
|
-
|
|
57
|
+
* activeTools 必须与下一次 chat() 实际发送的工具集合一致,避免 plan/子 agent 误算 schema。
|
|
58
|
+
* ephemeralTokens:本次请求会追加、但不在 history 里的尾部注入(会话状态提醒等)的裸 token,
|
|
59
|
+
* 必须传入,否则压力线看不见这部分开销(见 SystemCostBreakdown.ephemeral)。 */
|
|
60
|
+
export function evaluateBudget(history, window, step = 0, correction = 1, activeTools = chatTools, ephemeralTokens = 0) {
|
|
59
61
|
const layers = {};
|
|
60
62
|
for (const k of BUDGET_LAYERS) {
|
|
61
63
|
const budget = Math.floor(BUDGET_RATIO[k] * window);
|
|
@@ -66,12 +68,17 @@ export function evaluateBudget(history, window, step = 0, correction = 1, active
|
|
|
66
68
|
// 裸总量(不乘 correction):硬闸用它判断,防止 correction 折扣否决真实溢出。
|
|
67
69
|
let rawTotal = 0;
|
|
68
70
|
const sysMsg = history[0];
|
|
71
|
+
const safeEphemeral = Number.isFinite(ephemeralTokens)
|
|
72
|
+
? Math.max(0, Math.round(ephemeralTokens))
|
|
73
|
+
: 0;
|
|
69
74
|
const systemCosts = {
|
|
70
75
|
prompt: sysMsg ? msgTokens(sysMsg) : 0,
|
|
71
76
|
toolSchemas: estimateToolSchemaTokens(activeTools),
|
|
77
|
+
ephemeralInjection: safeEphemeral,
|
|
72
78
|
};
|
|
73
|
-
// 工具 schema
|
|
74
|
-
|
|
79
|
+
// 工具 schema、system prompt 与尾部 ephemeral 注入同属请求固定开销;
|
|
80
|
+
// 必须计入总量才能可靠触发压缩(尾部注入不在 history 里,只能由调用方传入)。
|
|
81
|
+
const systemRaw = systemCosts.prompt + systemCosts.toolSchemas + systemCosts.ephemeralInjection;
|
|
75
82
|
layers.system.actual = adj(systemRaw);
|
|
76
83
|
rawTotal += systemRaw;
|
|
77
84
|
// Summary 检测:role:'system' 且不是 history[0] 的,视为摘要(compact.ts 摘要插 index 1)。
|
|
@@ -143,7 +150,7 @@ export function scheduleActions(report) {
|
|
|
143
150
|
const actions = [];
|
|
144
151
|
const { layers } = report;
|
|
145
152
|
if (layers.system.overBudget) {
|
|
146
|
-
const { prompt, toolSchemas } = report.systemCosts;
|
|
153
|
+
const { prompt, toolSchemas, ephemeralInjection } = report.systemCosts;
|
|
147
154
|
const { actual, budget } = layers.system;
|
|
148
155
|
const excess = actual - budget;
|
|
149
156
|
const percent = ((actual / Math.max(budget, 1)) * 100).toFixed(0);
|
|
@@ -151,7 +158,9 @@ export function scheduleActions(report) {
|
|
|
151
158
|
kind: 'warn',
|
|
152
159
|
layer: 'system',
|
|
153
160
|
reason: `固定开销 ${actual}/${budget} (+${excess}, ${percent}%);`
|
|
154
|
-
+ `提示 ${prompt} + 工具 ${toolSchemas}
|
|
161
|
+
+ `提示 ${prompt} + 工具 ${toolSchemas}`
|
|
162
|
+
+ (ephemeralInjection > 0 ? ` + 尾部注入 ${ephemeralInjection}` : '')
|
|
163
|
+
+ `,×${report.correction.toFixed(2)}。`,
|
|
155
164
|
});
|
|
156
165
|
}
|
|
157
166
|
const pressureLine = DEFAULT_BUDGET_POLICY.pressureTriggerRatio * report.window;
|
package/dist/context/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// scheduler is the only automatic rewrite coordinator at real pressure.
|
|
4
4
|
export { optimizeToolResult } from './pipeline.js';
|
|
5
5
|
export { classify, knownToolKinds } from './classifier.js';
|
|
6
|
-
export { recordArtifact, invalidateArtifacts, rehydrateArtifacts, refreshArtifactFreshness, pruneStaleArtifacts, collectArtifactRefs, formatArtifactTokenSources, } from './artifacts.js';
|
|
6
|
+
export { recordArtifact, invalidateArtifacts, rehydrateArtifacts, refreshArtifactFreshness, pruneStaleArtifacts, collectArtifactRefs, formatArtifactTokenSources, knownEditTargets, } from './artifacts.js';
|
|
7
7
|
export { registerEncoder, registerAll, getEncoder, registeredKinds, } from './registry.js';
|
|
8
8
|
// ── Context Budget Scheduler ───────────────────────────────────────────────
|
|
9
9
|
export { evaluateBudget, scheduleActions, formatReport, quickEstimate, userTurnBoundary, BUDGET_LAYERS, DEFAULT_BUDGET_POLICY, BUDGET_RATIO, HOT_TURN_WINDOW, } from './budget.js';
|
package/dist/host/stdio.js
CHANGED
|
@@ -33,7 +33,12 @@ async function initializeRuntime() {
|
|
|
33
33
|
await initializeAllMcp();
|
|
34
34
|
registerToolsExtension('mcp', getMcpTools());
|
|
35
35
|
refreshChatTools();
|
|
36
|
-
emit('runtime_ready', {
|
|
36
|
+
emit('runtime_ready', {
|
|
37
|
+
projectRoot: process.cwd(),
|
|
38
|
+
provider: config.provider,
|
|
39
|
+
promptCache: config.provider === 'anthropic' && config.anthropicPromptCache,
|
|
40
|
+
warnings: getMcpWarnings(),
|
|
41
|
+
});
|
|
37
42
|
})();
|
|
38
43
|
return initialized;
|
|
39
44
|
}
|
|
@@ -117,6 +122,8 @@ async function run(command) {
|
|
|
117
122
|
sessionId,
|
|
118
123
|
projectRoot: process.cwd(),
|
|
119
124
|
resumed,
|
|
125
|
+
provider: config.provider,
|
|
126
|
+
promptCache: config.provider === 'anthropic' && config.anthropicPromptCache,
|
|
120
127
|
attachments: command.attachments?.map((attachment) => attachment.name) ?? [],
|
|
121
128
|
}, command.id);
|
|
122
129
|
const result = await runAgentCore({
|
|
@@ -134,6 +141,8 @@ async function run(command) {
|
|
|
134
141
|
terminationReason: result.terminationReason,
|
|
135
142
|
changedFiles: result.changedFiles ?? [],
|
|
136
143
|
usage: result.usage,
|
|
144
|
+
provider: config.provider,
|
|
145
|
+
promptCache: config.provider === 'anthropic' && config.anthropicPromptCache,
|
|
137
146
|
usagePercent: Math.round(contextUsagePercent() * 100),
|
|
138
147
|
contextWindow: config.contextWindowTokens,
|
|
139
148
|
}, command.id);
|
package/dist/llm/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import { config, isSubAgentEnabled, isFrontendToolsEnabled } from '../config/index.js';
|
|
2
|
+
import { config, getActiveModel, isSubAgentEnabled, isFrontendToolsEnabled } from '../config/index.js';
|
|
3
3
|
import { tools } from '../tools/registry.js';
|
|
4
4
|
import { getPlanDisabledTools, FRONTEND_TOOLS } from '../tools/constants.js';
|
|
5
5
|
import { ThinkTagFilter } from './think-filter.js';
|
|
6
|
+
import { anthropicChatOnce } from './providers/anthropic.js';
|
|
6
7
|
// 强制关闭第三方调试日志泄漏:openai SDK 在 process.env.DEBUG === 'true' 时用裸
|
|
7
8
|
// console.log 把请求/响应直写 stdout,会污染 TUI 输入框(并泄露 headers/URL)。
|
|
8
9
|
// 仅拦截 'true' 这一开关值——保留 namespace 形式的 DEBUG(如 DEBUG=express:*) 调试能力。
|
|
@@ -267,6 +268,9 @@ toolsOverride) {
|
|
|
267
268
|
throw new DOMException('This operation was aborted', 'AbortError');
|
|
268
269
|
}
|
|
269
270
|
try {
|
|
271
|
+
if (config.provider === 'anthropic') {
|
|
272
|
+
return await anthropicChatOnce(messages, handlers, signal, toolsOverride ?? chatTools);
|
|
273
|
+
}
|
|
270
274
|
return await chatOnce(messages, handlers, signal, toolsOverride);
|
|
271
275
|
}
|
|
272
276
|
catch (err) {
|
|
@@ -327,6 +331,22 @@ export function normalizeImageDetail(messages) {
|
|
|
327
331
|
}
|
|
328
332
|
/** 单次流式 LLM 请求(无重试);chat() 的内部实现,可被 __setChatCreateImpl 注入桩以做单测。 */
|
|
329
333
|
async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
334
|
+
// 防御:messages 必须至少含一条非空 user 消息,否则 OpenAI/Anthropic 都会 400。
|
|
335
|
+
// compact force 分支曾把所有 user 丢进摘要 → 重建 history 无 user → 下一轮 400。
|
|
336
|
+
// 在 transport 边界拦住所有类似回归(compact / 外部注入 / resume 损坏)。
|
|
337
|
+
const hasNonEmptyUser = messages.some((m) => {
|
|
338
|
+
if (m.role !== 'user')
|
|
339
|
+
return false;
|
|
340
|
+
const c = m.content;
|
|
341
|
+
if (typeof c === 'string')
|
|
342
|
+
return c.length > 0;
|
|
343
|
+
if (Array.isArray(c))
|
|
344
|
+
return c.some((p) => p.type === 'text' && (p.text?.length ?? 0) > 0);
|
|
345
|
+
return false;
|
|
346
|
+
});
|
|
347
|
+
if (!hasNonEmptyUser) {
|
|
348
|
+
throw new Error('messages must contain at least one non-empty user message');
|
|
349
|
+
}
|
|
330
350
|
// signal 透传给 SDK 第二参(RequestOptions);abort 后 for await 抛错,chat 不 catch,透传 runAgent 处理。
|
|
331
351
|
// createImplOverride 的 body 类型故意宽成 Record(测试桩用),生产走 client 分支时由 OpenAI 自己的类型守门。
|
|
332
352
|
const create = createImplOverride
|
|
@@ -334,7 +354,7 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
|
334
354
|
: (body, opts) => client.chat.completions.create(body, opts);
|
|
335
355
|
const activeTools = toolsOverride ?? chatTools;
|
|
336
356
|
const stream = await create({
|
|
337
|
-
model:
|
|
357
|
+
model: getActiveModel(),
|
|
338
358
|
messages: normalizeImageDetail(messages),
|
|
339
359
|
tools: activeTools,
|
|
340
360
|
stream: true,
|