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/context/pipeline.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
// Context Optimization Pipeline
|
|
1
|
+
// Typed Context Optimization Pipeline.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// 3) getEncoder(kind) ?? passthrough → encode(保不变量压缩,纯函数)。
|
|
8
|
-
// 4) capToolResultForHistory(name, text) 作末尾长度裁剪兜底(保 head+标记+tail,与改造前一致)。
|
|
3
|
+
// Normal tool insertion does not call this module: agent/core stores the raw
|
|
4
|
+
// result after only capToolResultForHistory(). The pressure scheduler invokes
|
|
5
|
+
// this encoder for Cold logs and retrievable searches when the opt-in switch is
|
|
6
|
+
// enabled. Encoder failures always fall back to the raw hard-capped result.
|
|
9
7
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// 兼容:不改 Tool Calling JSON schema、不改 executeTool、不改 tool_call_id 配对、不改 TUI 渲染
|
|
14
|
-
// (hooks.onToolResult 用原始 output,本函数只管进 history 的 content)。
|
|
15
|
-
//
|
|
16
|
-
// 依赖方向:context → {tools/constants, session/compact 的 cap, config};叶子,不反向依赖 llm/agent/tools。
|
|
8
|
+
// This does not alter tool schemas, execution, tool_call_id pairing, or TUI
|
|
9
|
+
// rendering; it only provides a pressure-stage representation transform.
|
|
17
10
|
import { classify } from './classifier.js';
|
|
18
11
|
import { getEncoder, registerAll } from './registry.js';
|
|
19
12
|
import { builtinEncoders } from './encoders/index.js';
|
|
@@ -60,7 +53,7 @@ function budgetFor(name) {
|
|
|
60
53
|
*/
|
|
61
54
|
export function optimizeToolResult(name, output, argsRaw, context = {}) {
|
|
62
55
|
boot();
|
|
63
|
-
//
|
|
56
|
+
// Disabled by default: normal history is raw apart from the hard cap.
|
|
64
57
|
if (!config.contextOptimize) {
|
|
65
58
|
return capToolResultForHistory(name, output);
|
|
66
59
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Relevance Pruner: statically removes tool results that a newer observation supersedes.
|
|
2
2
|
// It never deletes messages or changes tool_call_id pairing; only tool content is stubbed.
|
|
3
|
-
import { canonicalizePath, extractPath, isToolResultSuccess,
|
|
3
|
+
import { canonicalizePath, extractPath, isToolResultSuccess, toText, } from './utils.js';
|
|
4
4
|
/** Shared prefix lets /context count read and observation supersession together. */
|
|
5
5
|
const STUB_PREFIX = '⌦[已过时:';
|
|
6
6
|
const READ_STUB_REASON = '同 path 已有新 read / 已被 mutation 覆写';
|
|
@@ -72,7 +72,6 @@ export class RelevancePruner {
|
|
|
72
72
|
const path = canonicalizePath(extractPath(call.argsRaw));
|
|
73
73
|
if (!path)
|
|
74
74
|
return;
|
|
75
|
-
this.stubPriorReads(history, path, idx);
|
|
76
75
|
const list = this.readByPath.get(path) ?? [];
|
|
77
76
|
list.push(idx);
|
|
78
77
|
this.readByPath.set(path, list);
|
|
@@ -81,7 +80,6 @@ export class RelevancePruner {
|
|
|
81
80
|
const key = observationKey(call);
|
|
82
81
|
if (!key)
|
|
83
82
|
return;
|
|
84
|
-
this.stubPriorObservations(history, call.name, key, idx);
|
|
85
83
|
const list = this.observationByKey.get(key) ?? [];
|
|
86
84
|
list.push(idx);
|
|
87
85
|
this.observationByKey.set(key, list);
|
|
@@ -107,85 +105,109 @@ export class RelevancePruner {
|
|
|
107
105
|
}
|
|
108
106
|
return null;
|
|
109
107
|
}
|
|
110
|
-
observeMutation(
|
|
108
|
+
observeMutation(_history, _path) {
|
|
109
|
+
// Mutations are retained as provenance. pruneSuperseded() derives their
|
|
110
|
+
// impact only when the scheduler enters real context pressure.
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Pressure-only cleanup. Scan the complete history to identify evidence that
|
|
114
|
+
* has an exact newer replacement, but only rewrite messages before the Cold
|
|
115
|
+
* boundary. This keeps the latest four user turns and current work intact.
|
|
116
|
+
*/
|
|
117
|
+
pruneSuperseded(history, coldBoundary) {
|
|
111
118
|
try {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
const latestRead = new Map();
|
|
120
|
+
const latestObservation = new Map();
|
|
121
|
+
const mutations = [];
|
|
122
|
+
for (let idx = 1; idx < history.length; idx++) {
|
|
123
|
+
const message = history[idx];
|
|
124
|
+
const content = toText(message.content);
|
|
125
|
+
if (message.role !== 'tool' || content.startsWith('⌦[') || !isToolResultSuccess(content))
|
|
126
|
+
continue;
|
|
127
|
+
const call = this.callAt(history, idx);
|
|
128
|
+
if (!call)
|
|
129
|
+
continue;
|
|
130
|
+
if (call.name === 'read_file') {
|
|
131
|
+
const path = canonicalizePath(extractPath(call.argsRaw));
|
|
132
|
+
if (path)
|
|
133
|
+
latestRead.set(path, idx);
|
|
134
|
+
}
|
|
135
|
+
else if (call.name === 'edit_file' || call.name === 'write_file') {
|
|
136
|
+
const path = canonicalizePath(extractPath(call.argsRaw));
|
|
137
|
+
if (path)
|
|
138
|
+
mutations.push({ path, index: idx });
|
|
139
|
+
}
|
|
140
|
+
const key = observationKey(call);
|
|
141
|
+
if (key)
|
|
142
|
+
latestObservation.set(key, { tool: call.name, index: idx });
|
|
143
|
+
}
|
|
144
|
+
let pruned = 0;
|
|
145
|
+
for (const [path, index] of latestRead) {
|
|
146
|
+
pruned += this.stubPriorReads(history, path, index, coldBoundary);
|
|
147
|
+
}
|
|
148
|
+
for (const mutation of mutations) {
|
|
149
|
+
pruned += this.stubPriorReads(history, mutation.path, mutation.index, coldBoundary);
|
|
150
|
+
}
|
|
151
|
+
for (const [key, latest] of latestObservation) {
|
|
152
|
+
pruned += this.stubPriorObservations(history, latest.tool, key, latest.index, coldBoundary);
|
|
153
|
+
}
|
|
154
|
+
return pruned;
|
|
120
155
|
}
|
|
121
156
|
catch {
|
|
122
|
-
|
|
157
|
+
return 0;
|
|
123
158
|
}
|
|
124
159
|
}
|
|
125
|
-
stubPriorReads(history, path, beforeIdx) {
|
|
160
|
+
stubPriorReads(history, path, beforeIdx, coldBoundary) {
|
|
126
161
|
const targetPath = canonicalizePath(path);
|
|
127
162
|
if (!targetPath)
|
|
128
|
-
return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (idx >= beforeIdx || (protectedFrom > 0 && idx >= protectedFrom))
|
|
132
|
-
return;
|
|
163
|
+
return 0;
|
|
164
|
+
let pruned = 0;
|
|
165
|
+
for (let idx = 1; idx < Math.min(beforeIdx, coldBoundary); idx++) {
|
|
133
166
|
const message = history[idx];
|
|
134
167
|
if (!message || message.role !== 'tool')
|
|
135
|
-
|
|
168
|
+
continue;
|
|
136
169
|
const content = toText(message.content);
|
|
137
|
-
if (content.startsWith(
|
|
138
|
-
|
|
170
|
+
if (content.startsWith('⌦['))
|
|
171
|
+
continue;
|
|
139
172
|
const call = this.callAt(history, idx);
|
|
140
173
|
if (call?.name !== 'read_file')
|
|
141
|
-
|
|
142
|
-
if (canonicalizePath(extractPath(call.argsRaw)) !== targetPath)
|
|
143
|
-
|
|
144
|
-
if (!message.tool_call_id)
|
|
145
|
-
return;
|
|
174
|
+
continue;
|
|
175
|
+
if (canonicalizePath(extractPath(call.argsRaw)) !== targetPath || !message.tool_call_id)
|
|
176
|
+
continue;
|
|
146
177
|
message.content =
|
|
147
178
|
`${STUB_PREFIX}${READ_STUB_REASON}] read_file(${targetPath}) ${content.length} 字符 ` +
|
|
148
179
|
`→ 已被新 read / mutation 替代 · id …${message.tool_call_id.slice(-6)}⌫`;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const scanEnd = Math.min(beforeIdx, protectedFrom > 0 ? protectedFrom : beforeIdx);
|
|
153
|
-
for (let idx = 1; idx < scanEnd; idx++)
|
|
154
|
-
stubOne(idx);
|
|
180
|
+
pruned++;
|
|
181
|
+
}
|
|
182
|
+
return pruned;
|
|
155
183
|
}
|
|
156
|
-
stubPriorObservations(history, toolName, key, beforeIdx) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (idx >= beforeIdx || (protectedFrom > 0 && idx >= protectedFrom))
|
|
160
|
-
return;
|
|
184
|
+
stubPriorObservations(history, toolName, key, beforeIdx, coldBoundary) {
|
|
185
|
+
let pruned = 0;
|
|
186
|
+
for (let idx = 1; idx < Math.min(beforeIdx, coldBoundary); idx++) {
|
|
161
187
|
const message = history[idx];
|
|
162
188
|
if (!message || message.role !== 'tool')
|
|
163
|
-
|
|
189
|
+
continue;
|
|
164
190
|
const content = toText(message.content);
|
|
165
|
-
if (content.startsWith(
|
|
166
|
-
|
|
191
|
+
if (content.startsWith('⌦[') || !isToolResultSuccess(content))
|
|
192
|
+
continue;
|
|
167
193
|
const call = this.callAt(history, idx);
|
|
168
|
-
if (!call || call.name !== toolName || observationKey(call) !== key)
|
|
169
|
-
|
|
170
|
-
if (!message.tool_call_id)
|
|
171
|
-
return;
|
|
172
|
-
const reason = '相同 grep 查询已有更新结果';
|
|
194
|
+
if (!call || call.name !== toolName || observationKey(call) !== key || !message.tool_call_id)
|
|
195
|
+
continue;
|
|
173
196
|
message.content =
|
|
174
|
-
`${STUB_PREFIX}
|
|
197
|
+
`${STUB_PREFIX}相同 grep 查询已有更新结果] ${observationLabel(call)} ${content.length} 字符 ` +
|
|
175
198
|
`→ 已被更新查询替代 · id …${message.tool_call_id.slice(-6)}⌫`;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// The fallback scan restores correctness after resume/compact when this instance
|
|
180
|
-
// has no index for older messages.
|
|
181
|
-
const scanEnd = Math.min(beforeIdx, protectedFrom > 0 ? protectedFrom : beforeIdx);
|
|
182
|
-
for (let idx = 1; idx < scanEnd; idx++)
|
|
183
|
-
stubOne(idx);
|
|
199
|
+
pruned++;
|
|
200
|
+
}
|
|
201
|
+
return pruned;
|
|
184
202
|
}
|
|
185
203
|
}
|
|
186
204
|
export function createRelevancePruner() {
|
|
187
205
|
return new RelevancePruner();
|
|
188
206
|
}
|
|
207
|
+
/** Pressure-only convenience entry point for scheduler-owned pruning. */
|
|
208
|
+
export function pruneSuperseded(history, coldBoundary) {
|
|
209
|
+
return new RelevancePruner().pruneSuperseded(history, coldBoundary);
|
|
210
|
+
}
|
|
189
211
|
/** Parse the original content length recorded by any relevance stub. */
|
|
190
212
|
function parseStubOriginalLen(stub) {
|
|
191
213
|
const match = /\) (\d+) 字符 →/.exec(stub);
|
package/dist/host/stdio.js
CHANGED
|
@@ -8,7 +8,6 @@ import { initializeAllMcp, getMcpTools, getMcpWarnings, closeAllMcp } from '../m
|
|
|
8
8
|
import { setSandboxRoot } from '../sandbox/index.js';
|
|
9
9
|
import { createContextState, loadSession, newSessionId, saveSession } from '../session/index.js';
|
|
10
10
|
import { setCurrentSessionId } from '../session/state.js';
|
|
11
|
-
import { buildActiveNotesPlanReminder } from '../session/notes-plan.js';
|
|
12
11
|
import { manualCompact } from '../session/scheduler.js';
|
|
13
12
|
import { effectiveSystemPrompt } from '../skills/index.js';
|
|
14
13
|
import { registerToolsExtension } from '../tools/registry.js';
|
|
@@ -91,8 +90,6 @@ function hooksFor(runId) {
|
|
|
91
90
|
onToolHeader: (tool) => emit('tool_started', { id: tool.id, name: tool.name, arguments: tool.arguments }, runId),
|
|
92
91
|
onToolStart: (name) => emit('status', { value: 'running_tool', tool: name }, runId),
|
|
93
92
|
onToolResult: (tool, output) => emit('tool_completed', { id: tool.id, name: tool.name, output }, runId),
|
|
94
|
-
onValidationStart: (command) => emit('validation_started', { command }, runId),
|
|
95
|
-
onValidationResult: (result) => emit('validation_completed', { result }, runId),
|
|
96
93
|
onAbort: () => emit('run_aborted', {}, runId),
|
|
97
94
|
onDone: (elapsedMs, usage) => emit('run_finished', { elapsedMs, usage }, runId),
|
|
98
95
|
};
|
|
@@ -126,10 +123,8 @@ async function run(command) {
|
|
|
126
123
|
history,
|
|
127
124
|
userInput,
|
|
128
125
|
signal: controller.signal,
|
|
129
|
-
dynamicSystemSuffix: buildActiveNotesPlanReminder,
|
|
130
126
|
hooks: hooksFor(command.id),
|
|
131
127
|
contextState,
|
|
132
|
-
autoValidate: config.autoValidate,
|
|
133
128
|
permissionPrompt: (request) => waitForApproval(command.id, request),
|
|
134
129
|
});
|
|
135
130
|
saveSession(history, sessionId, queryHistory);
|
|
@@ -138,7 +133,6 @@ async function run(command) {
|
|
|
138
133
|
completed: result.completed,
|
|
139
134
|
terminationReason: result.terminationReason,
|
|
140
135
|
changedFiles: result.changedFiles ?? [],
|
|
141
|
-
validation: result.validation,
|
|
142
136
|
usage: result.usage,
|
|
143
137
|
usagePercent: Math.round(contextUsagePercent() * 100),
|
|
144
138
|
contextWindow: config.contextWindowTokens,
|
package/dist/i18n/index.js
CHANGED
|
@@ -156,9 +156,6 @@ const zhCN = {
|
|
|
156
156
|
'agent.noReply': '(无回复)',
|
|
157
157
|
'agent.maxSteps': '达到最大步数({count}),本轮停止。',
|
|
158
158
|
'agent.aborted': '(已中断)',
|
|
159
|
-
'agent.validating': '自动验证 {command}',
|
|
160
|
-
'agent.validationNoCommand': '未发现验证命令',
|
|
161
|
-
'agent.validationResult': '自动验证 {command} → {status}',
|
|
162
159
|
'agent.workedFor': '耗时 {elapsed}',
|
|
163
160
|
'agent.toolsRunning': '正在探索',
|
|
164
161
|
'agent.toolsComplete': '探索',
|
|
@@ -401,9 +398,6 @@ const en = {
|
|
|
401
398
|
'agent.noReply': '(no reply)',
|
|
402
399
|
'agent.maxSteps': 'Maximum steps reached ({count}); this turn has stopped.',
|
|
403
400
|
'agent.aborted': '(aborted)',
|
|
404
|
-
'agent.validating': 'Validating {command}',
|
|
405
|
-
'agent.validationNoCommand': 'no validation command',
|
|
406
|
-
'agent.validationResult': 'Automatic validation {command} → {status}',
|
|
407
401
|
'agent.workedFor': 'Worked for {elapsed}',
|
|
408
402
|
'agent.toolsRunning': 'Exploring',
|
|
409
403
|
'agent.toolsComplete': 'Exploration',
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { exitAltScreen } from './ui/layout.js';
|
|
2
2
|
import { readConfigFile } from './config/file.js';
|
|
3
3
|
import { detectLanguage, setLanguage, t } from './i18n/index.js';
|
|
4
|
+
import { shutdownRuntime, shutdownRuntimeSync } from './runtime/shutdown.js';
|
|
4
5
|
// 终端恢复兜底:任一退出 / 中断 / 未捕获异常路径都要恢复 alt screen,避免残留备用屏 + 滚动区域。
|
|
5
6
|
// exitAltScreen 幂等(未激活时空操作),故全局注册安全——进 alt screen 前的路径(如 --resume 列表、缺环境变量、`mocode config`)调用它无副作用。
|
|
6
7
|
// 仅 layout 是叶子(不依赖 config),故静态导入安全;repl / session 依赖 config(模块加载触发 loadEnvFiles + config 单例初始化),
|
|
7
8
|
// 改动态按需加载——`mocode config` 向导只需读写文件(走 config/file.ts 叶子),不经 config 单例初始化,零配置也能跑。
|
|
8
|
-
|
|
9
|
+
// dev_server 拉起的后台进程不随父进程退出而消失(Windows 无 job object),故每条退出路径
|
|
10
|
+
// 都同步树杀一次;shutdownRuntimeSync 幂等。
|
|
11
|
+
process.on('exit', () => {
|
|
12
|
+
shutdownRuntimeSync();
|
|
13
|
+
exitAltScreen();
|
|
14
|
+
});
|
|
9
15
|
process.on('SIGINT', () => {
|
|
16
|
+
shutdownRuntimeSync();
|
|
10
17
|
exitAltScreen();
|
|
11
18
|
process.exit(130);
|
|
12
19
|
});
|
|
13
20
|
process.on('uncaughtException', (e) => {
|
|
21
|
+
shutdownRuntimeSync();
|
|
14
22
|
try {
|
|
15
23
|
process.stderr.write(`\n[uncaught] ${e instanceof Error ? e.stack || e.message : String(e)}\n`);
|
|
16
24
|
}
|
|
@@ -85,6 +93,8 @@ async function main() {
|
|
|
85
93
|
const { startRepl } = await import('./repl/index.js');
|
|
86
94
|
await startRepl(undefined, undefined, sandboxRootOverride);
|
|
87
95
|
}
|
|
96
|
+
// 正常退出:优雅关闭浏览器与后台进程(同步兜底仍在 exit 钩子里)。
|
|
97
|
+
await shutdownRuntime();
|
|
88
98
|
process.exit(0);
|
|
89
99
|
}
|
|
90
100
|
main();
|
package/dist/llm/index.js
CHANGED
|
@@ -286,6 +286,42 @@ toolsOverride) {
|
|
|
286
286
|
// 循环要么 return 要么 throw,理论上走不到这里;写出来让 TS 控制流分析满意。
|
|
287
287
|
throw lastErr;
|
|
288
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* 发送前规范化多模态 image_url:去掉 `detail:"auto"`。
|
|
291
|
+
*
|
|
292
|
+
* `auto` 是 OpenAI 的合法枚举,但 MiniMax 等兼容后端只认 low/default/high,收到 auto 直接
|
|
293
|
+
* 400(invalid image detail: auto, 2013)。省略该字段时各家都会用自己的默认值,是唯一
|
|
294
|
+
* 在所有后端都安全的写法;显式 low/high 属通用取值,原样保留。
|
|
295
|
+
*
|
|
296
|
+
* 放在 transport 边界而非构造点:历史里可能已经存着旧版本(或续接会话 / 外部注入)写下的
|
|
297
|
+
* `auto`,那种消息每轮都会被重发,只修构造点无法自愈。
|
|
298
|
+
*
|
|
299
|
+
* 无需改写时返回原数组引用 —— 图片消息含大段 base64,不能无条件深拷贝。
|
|
300
|
+
*/
|
|
301
|
+
export function normalizeImageDetail(messages) {
|
|
302
|
+
let changed = false;
|
|
303
|
+
const next = messages.map((message) => {
|
|
304
|
+
const content = message.content;
|
|
305
|
+
if (!Array.isArray(content))
|
|
306
|
+
return message;
|
|
307
|
+
let messageChanged = false;
|
|
308
|
+
const parts = content.map((part) => {
|
|
309
|
+
const image = part.image_url;
|
|
310
|
+
if (part.type !== 'image_url' || !image)
|
|
311
|
+
return part;
|
|
312
|
+
if (image.detail !== 'auto')
|
|
313
|
+
return part;
|
|
314
|
+
messageChanged = true;
|
|
315
|
+
const { detail: _dropped, ...rest } = image;
|
|
316
|
+
return { ...part, image_url: rest };
|
|
317
|
+
});
|
|
318
|
+
if (!messageChanged)
|
|
319
|
+
return message;
|
|
320
|
+
changed = true;
|
|
321
|
+
return { ...message, content: parts };
|
|
322
|
+
});
|
|
323
|
+
return changed ? next : messages;
|
|
324
|
+
}
|
|
289
325
|
/** 单次流式 LLM 请求(无重试);chat() 的内部实现,可被 __setChatCreateImpl 注入桩以做单测。 */
|
|
290
326
|
async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
291
327
|
// signal 透传给 SDK 第二参(RequestOptions);abort 后 for await 抛错,chat 不 catch,透传 runAgent 处理。
|
|
@@ -296,7 +332,7 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
|
296
332
|
const activeTools = toolsOverride ?? chatTools;
|
|
297
333
|
const stream = await create({
|
|
298
334
|
model: config.model,
|
|
299
|
-
messages,
|
|
335
|
+
messages: normalizeImageDetail(messages),
|
|
300
336
|
tools: activeTools,
|
|
301
337
|
stream: true,
|
|
302
338
|
// 显式声明允许一次响应携带多个 tool_call(OpenAI 兼容协议标准字段)。
|
|
@@ -319,6 +355,24 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
|
319
355
|
handlers.onText?.(text);
|
|
320
356
|
consumedAny = true;
|
|
321
357
|
};
|
|
358
|
+
// 实时 completion 估算(onProgress 提供时才统计,零开销兜底):
|
|
359
|
+
// 按 chunk 累加 CJK/other 字符数、汇总时才 ceil——逐片段 ceil 会把大量小 chunk 各向上
|
|
360
|
+
// 取整造成严重过估。统计口径 = raw content(含 think 段)+ tool_call 参数,与后端真实
|
|
361
|
+
// completion 计费范围一致(reasoning 也计费)。
|
|
362
|
+
let liveCjk = 0;
|
|
363
|
+
let liveOther = 0;
|
|
364
|
+
const countLive = (text) => {
|
|
365
|
+
for (const ch of text) {
|
|
366
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
367
|
+
if (isCJK(cp))
|
|
368
|
+
liveCjk++;
|
|
369
|
+
else
|
|
370
|
+
liveOther++;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const reportProgress = () => {
|
|
374
|
+
handlers.onProgress?.({ completionTokens: Math.ceil(liveCjk + liveOther / 4) });
|
|
375
|
+
};
|
|
322
376
|
for await (const chunk of stream) {
|
|
323
377
|
// usage:末尾 chunk(choices 可能为空)在 include_usage 时携带;先读再 continue。
|
|
324
378
|
if (chunk.usage) {
|
|
@@ -330,12 +384,21 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
|
330
384
|
cachedTokens: extras.cachedTokens,
|
|
331
385
|
reasoningTokens: extras.reasoningTokens,
|
|
332
386
|
};
|
|
387
|
+
// 末尾 chunk 把实测 prompt / completion / cache 命中即时推给实时 chip(无 delta,下方 continue 不会再报)。
|
|
388
|
+
handlers.onProgress?.({
|
|
389
|
+
completionTokens: usage.completionTokens,
|
|
390
|
+
promptTokens: usage.promptTokens,
|
|
391
|
+
cachedTokens: usage.cachedTokens,
|
|
392
|
+
});
|
|
333
393
|
}
|
|
334
394
|
const delta = chunk.choices?.[0]?.delta;
|
|
335
395
|
if (!delta)
|
|
336
396
|
continue; // 末尾 usage-only chunk 等无 delta
|
|
337
|
-
if (delta.content)
|
|
397
|
+
if (delta.content) {
|
|
398
|
+
if (handlers.onProgress)
|
|
399
|
+
countLive(delta.content);
|
|
338
400
|
emitVisible(thinkFilter.push(delta.content));
|
|
401
|
+
}
|
|
339
402
|
if (delta.tool_calls) {
|
|
340
403
|
// 不在这里 flush thinkFilter:其内部若有残留,只可能是 `<th` / `</thi` 一类
|
|
341
404
|
// 潜在标签前缀。旧实现把这段在工具转折点强制送进 onText,正是 `k>` 等残片
|
|
@@ -355,10 +418,16 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
|
355
418
|
handlers.onToolCall?.(fname); // 首次得知工具名:通知调用方启生成中 spinner
|
|
356
419
|
entry.name += fname;
|
|
357
420
|
}
|
|
358
|
-
if (tc.function?.arguments)
|
|
421
|
+
if (tc.function?.arguments) {
|
|
422
|
+
if (handlers.onProgress)
|
|
423
|
+
countLive(tc.function.arguments);
|
|
359
424
|
entry.arguments += tc.function.arguments;
|
|
425
|
+
}
|
|
360
426
|
}
|
|
361
427
|
}
|
|
428
|
+
// 流式实时用量:每个带 delta 的 chunk 后回调累计估算,驱动底栏实时 chip。
|
|
429
|
+
if (handlers.onProgress)
|
|
430
|
+
reportProgress();
|
|
362
431
|
}
|
|
363
432
|
// 流结束后只释放普通态下真实的文本尾;未闭合思考段继续丢弃。
|
|
364
433
|
emitVisible(thinkFilter.finish());
|
package/dist/mcp/index.js
CHANGED
package/dist/repl/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, hasCodegraphIndex, } from '../config/index.js';
|
|
4
|
+
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, hasCodegraphIndex, DEFAULT_CONTEXT_WINDOW_TOKENS, } from '../config/index.js';
|
|
5
5
|
import { getLanguage, normalizeLanguage, t, } from '../i18n/index.js';
|
|
6
|
+
import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
|
|
6
7
|
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
7
8
|
import { deletePreset, getPreset, isValidPresetName, listPresets, migrateCurrentToPreset, savePreset, } from '../config/presets.js';
|
|
8
9
|
import { runAgent } from '../agent/index.js';
|
|
@@ -142,15 +143,15 @@ function themeDescription(name) {
|
|
|
142
143
|
}
|
|
143
144
|
/** /model 预设后端:选一个预填 baseURL,仍可逐项改。base_url 取自 README 常见表。 */
|
|
144
145
|
const MODEL_PRESETS = [
|
|
145
|
-
{ label: 'GLM(智谱)', baseURL: 'https://open.bigmodel.cn/api/v3', model: 'glm-4.6', window:
|
|
146
|
-
{ label: 'DeepSeek', baseURL: 'https://api.deepseek.com', model: 'deepseek-chat', window:
|
|
147
|
-
{ label: 'Qwen(阿里)', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus', window:
|
|
146
|
+
{ label: 'GLM(智谱)', baseURL: 'https://open.bigmodel.cn/api/v3', model: 'glm-4.6', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
147
|
+
{ label: 'DeepSeek', baseURL: 'https://api.deepseek.com', model: 'deepseek-chat', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
148
|
+
{ label: 'Qwen(阿里)', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
148
149
|
// MiniMax OpenAI 兼容端点(https://platform.minimax.io/docs/api-reference/text-openai-api)。
|
|
149
150
|
// MiniMax-M3 为唯一支持图片/视频输入的模型;M2 系列纯文本(见 llm/capabilities.ts KNOWN_TEXT_ONLY_PREFIXES)。
|
|
150
|
-
{ label: 'MiniMax', baseURL: 'https://api.minimax.io/v1', model: 'MiniMax-M3', window:
|
|
151
|
-
{ label: '本地 Ollama', baseURL: 'http://localhost:11434/v1', model: 'qwen2.5:7b', window:
|
|
152
|
-
{ label: '本地 vLLM', baseURL: 'http://localhost:8000/v1', model: 'default', window:
|
|
153
|
-
{ label: '自定义 base_url', baseURL: '', model: '', window:
|
|
151
|
+
{ label: 'MiniMax', baseURL: 'https://api.minimax.io/v1', model: 'MiniMax-M3', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
152
|
+
{ label: '本地 Ollama', baseURL: 'http://localhost:11434/v1', model: 'qwen2.5:7b', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
153
|
+
{ label: '本地 vLLM', baseURL: 'http://localhost:8000/v1', model: 'default', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
154
|
+
{ label: '自定义 base_url', baseURL: '', model: '', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
154
155
|
];
|
|
155
156
|
/** apiKey 脱敏:只露末 4 位,前面打星号(显示用,绝不把明文 key 写进内容区)。 */
|
|
156
157
|
function maskKey(k) {
|
|
@@ -217,7 +218,7 @@ function renderContextBar(history) {
|
|
|
217
218
|
const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
|
|
218
219
|
const src = contextState.lastUsage ? t('status.measured') : t('status.estimated');
|
|
219
220
|
const k = (n) => `${Math.round(n / 1000)}k`;
|
|
220
|
-
const pctCol = pct >=
|
|
221
|
+
const pctCol = pct >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio ? ui.yellow : ui.accent;
|
|
221
222
|
const lifecycle = contextState.lifecycleStats;
|
|
222
223
|
const archived = computePruneStats(history);
|
|
223
224
|
const artifactStats = contextState.artifactStats;
|
|
@@ -242,7 +243,7 @@ function renderContextBarInline(history) {
|
|
|
242
243
|
const filled = Math.round(pct * W);
|
|
243
244
|
const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
|
|
244
245
|
const k = (n) => `${Math.round(n / 1000)}k`;
|
|
245
|
-
const pctCol = pct >=
|
|
246
|
+
const pctCol = pct >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio ? ui.yellow : ui.accent;
|
|
246
247
|
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${pctCol}${Math.round(pct * 100)}%${ui.reset} ${ui.dim}${k(est)}/${k(win)}${ui.reset}`;
|
|
247
248
|
}
|
|
248
249
|
// 宿主侧记录已结束轮次最后看到的 plan。notes.md 仍完整保留,只抑制未变化的旧 plan 状态栏,
|
|
@@ -1031,10 +1032,14 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1031
1032
|
// 直接给原文对中文用户不友好。这里翻译成中文 + 提示 /model 换视觉模型。
|
|
1032
1033
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1033
1034
|
const lower = msg.toLowerCase();
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1035
|
+
// 只有明确的「能力不支持」才显示换模型提示。不能仅因错误里出现 image/vision
|
|
1036
|
+
// 就下结论:例如 MiniMax 会因 `image_url.detail=auto` 返 400 invalid params,
|
|
1037
|
+
// 模型本身仍支持视觉。参数/格式错误应保留原始 provider 诊断,方便准确修复。
|
|
1038
|
+
const isImageParameterError = /\b(?:invalid|unsupported)\s+(?:image\s+)?(?:detail|parameter|param|format|url)\b/i.test(lower);
|
|
1039
|
+
const isVisionUnsupportedError = !isImageParameterError && (/\b(?:does not support|doesn't support|not supported|unsupported)\b.{0,48}\b(?:image|vision|multimodal|media)\b/i.test(lower) ||
|
|
1040
|
+
/\b(?:image|vision|multimodal|media)\b.{0,48}\b(?:is not supported|not supported|unsupported)\b/i.test(lower) ||
|
|
1041
|
+
/(?:不支持|不具备).{0,12}(?:视觉|图片|图像|多模态)|(?:视觉|图片|图像|多模态).{0,12}(?:不支持|不可用)/.test(msg));
|
|
1042
|
+
if (isVisionUnsupportedError) {
|
|
1038
1043
|
layout.contentWrite(`${ui.red}${t('repl.errorLabel')}${ui.reset} ${t('repl.visionUnsupported', { model: `${ui.accent}${config.model}${ui.reset}` })}${ui.dim}${t('repl.originalError', { message: msg })}${ui.reset}\n`);
|
|
1039
1044
|
layout.contentWrite(`${ui.dim}${t('repl.visionHint')}${ui.reset}\n`);
|
|
1040
1045
|
}
|
|
@@ -1049,6 +1054,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1049
1054
|
const waitingForPlanApproval = ok && planMode && getAgentMode() === 'plan';
|
|
1050
1055
|
if (!waitingForPlanApproval)
|
|
1051
1056
|
settlePlanStatus();
|
|
1057
|
+
layout.setLiveUsage(undefined); // 轮末清实时 chip,回 INPUT 态不再显示
|
|
1052
1058
|
refreshStatusBase(history, lastTurnUsage);
|
|
1053
1059
|
layout.drawStatusBar();
|
|
1054
1060
|
}
|
|
@@ -1915,7 +1921,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1915
1921
|
const res = await promptIntervention({
|
|
1916
1922
|
type: 'input',
|
|
1917
1923
|
title: 'CONTEXT_WINDOW_TOKENS',
|
|
1918
|
-
detail: '
|
|
1924
|
+
detail: '模型上下文窗口,全局默认 256k;如需不同窗口可手动覆盖。回车采纳预填值。',
|
|
1919
1925
|
seed: String(preset.window || config.contextWindowTokens),
|
|
1920
1926
|
});
|
|
1921
1927
|
if (res.action === 'cancelled') {
|