mocode-ai 1.7.10 → 1.8.0
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/index.js +50 -14
- package/dist/agent/run-coordinator.js +10 -4
- package/dist/agent/stages/tool-dispatcher.js +1 -0
- package/dist/agent/tool-helpers.js +55 -10
- package/dist/config/file.js +11 -3
- package/dist/config/index.js +16 -3
- package/dist/config/presets.js +9 -0
- package/dist/headless.js +8 -1
- package/dist/index.js +1 -1
- package/dist/llm/index.js +10 -1
- package/dist/llm/providers/anthropic.js +9 -1
- package/dist/llm/providers/vertex.js +7 -1
- package/dist/memory/graph.js +262 -1
- package/dist/memory/index.js +3 -1
- package/dist/memory/persona.js +142 -0
- package/dist/memory/recall-stats.js +61 -0
- package/dist/memory/recall.js +47 -0
- package/dist/memory/reflect.js +112 -17
- package/dist/memory/relation-vocab.js +84 -0
- package/dist/models/map-preset.js +4 -0
- package/dist/repl/commands/model-actions.js +3 -0
- package/dist/repl/commands/model.js +39 -3
- package/dist/repl/runtime.js +13 -4
- package/dist/runtime/runtime.js +1 -1
- package/dist/session/compact-archive.js +232 -0
- package/dist/session/compact.js +19 -2
- package/dist/session/persist.js +4 -3
- package/dist/session/retention.js +33 -2
- package/dist/session/store.js +187 -3
- package/dist/tools/builtins/edit-file.js +110 -22
- package/dist/tools/builtins/grep.js +5 -2
- package/dist/tools/builtins/memory-graph.js +2 -0
- package/dist/tools/builtins/memory-search.js +20 -14
- package/dist/tools/builtins/write-file.js +4 -2
- package/dist/tools/constants.js +4 -0
- package/dist/tools/ripgrep-client.js +11 -1
- package/dist/tools/tool-runtime.js +65 -2
- package/dist/ui/diff.js +68 -4
- package/package.json +1 -1
package/dist/agent/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { ui } from '../ui/theme.js';
|
|
6
6
|
import { Spinner } from '../ui/spinner.js';
|
|
7
7
|
import { summarizeToolCall, summarizeToolResult, truncateDisplay, fmtElapsed } from '../ui/render.js';
|
|
8
|
-
import { renderFileChange } from '../ui/diff.js';
|
|
8
|
+
import { renderFileChange, renderMultiFileChange } from '../ui/diff.js';
|
|
9
9
|
import * as layout from '../ui/layout.js';
|
|
10
10
|
import * as batch from '../ui/batch.js';
|
|
11
11
|
import { config } from '../config/index.js';
|
|
@@ -150,7 +150,7 @@ function writeToolHeader(tc, toolRuntime) {
|
|
|
150
150
|
}
|
|
151
151
|
/** 渲染工具结果:mutation 成功走 diff 块(行号 + 语法高亮);其余走一行 preview。
|
|
152
152
|
* 同 writeToolHeader,改为累积到 BatchRenderer(只缓存字符串,不写屏)。 */
|
|
153
|
-
function writeToolResult(tc, output, parsed, preWriteOld, editStartLine, toolRuntime) {
|
|
153
|
+
function writeToolResult(tc, output, parsed, preWriteOld, editStartLine, toolRuntime, multiHunks) {
|
|
154
154
|
// 优先按 tool_call id 反查所属批(并行 / sub-agent 组时 currentBatchId 已不是它)。
|
|
155
155
|
const batchId = batch.batchIdForCall(tc.id) ?? currentBatchId;
|
|
156
156
|
if (!batchId)
|
|
@@ -173,19 +173,55 @@ function writeToolResult(tc, output, parsed, preWriteOld, editStartLine, toolRun
|
|
|
173
173
|
}
|
|
174
174
|
let diff = null;
|
|
175
175
|
if ((tc.name === 'edit_file' || tc.name === 'write_file') && parsed && !isToolErrorOutput(output)) {
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
176
|
+
const filePath = String(parsed.path ?? '');
|
|
177
|
+
const isEdit = tc.name === 'edit_file';
|
|
178
|
+
// 多替换模式:每个 old/new 段一个 hunk,渲染成一个 Update 块内的多个 hunk。
|
|
179
|
+
const multi = isEdit && multiHunks && multiHunks.length > 0 ? multiHunks : null;
|
|
180
|
+
const oldText = isEdit
|
|
181
|
+
? multi
|
|
182
|
+
? multi.map((h) => h.oldStr).join('\n')
|
|
183
|
+
: String(parsed.old_string ?? '')
|
|
184
|
+
: preWriteOld;
|
|
185
|
+
const newText = isEdit
|
|
186
|
+
? multi
|
|
187
|
+
? multi.map((h) => h.newStr).join('\n')
|
|
188
|
+
: String(parsed.new_string ?? '')
|
|
189
|
+
: String(parsed.content ?? '');
|
|
190
|
+
if (multi) {
|
|
191
|
+
diff = renderMultiFileChange({
|
|
192
|
+
path: filePath,
|
|
193
|
+
hunks: multi.map((h) => ({ oldStr: h.oldStr, newStr: h.newStr, startLine: h.startLine })),
|
|
194
|
+
});
|
|
195
|
+
// 渲染器对超大 diff 返回 null(无法做 LCS)时退回工具结果摘要。
|
|
196
|
+
if (!diff)
|
|
197
|
+
diff = renderFileChange({
|
|
198
|
+
path: filePath,
|
|
199
|
+
kind: 'edit',
|
|
200
|
+
oldStr: oldText,
|
|
201
|
+
newStr: newText,
|
|
202
|
+
startLine: editStartLine,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
diff = renderFileChange({
|
|
207
|
+
path: filePath,
|
|
208
|
+
kind: isEdit ? 'edit' : 'write',
|
|
209
|
+
oldStr: isEdit ? String(parsed.old_string ?? '') : preWriteOld,
|
|
210
|
+
newStr: String((isEdit ? parsed.new_string : parsed.content) ?? ''),
|
|
211
|
+
startLine: isEdit ? editStartLine : 1,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// 回合改动统计:多替换按各 hunk 独立计算后累加(拼接不相邻段再 diff 会把跨段行误算成增删)。
|
|
215
|
+
const delta = multi
|
|
216
|
+
? multi.reduce((acc, h) => {
|
|
217
|
+
const d = lineDelta(h.oldStr, h.newStr);
|
|
218
|
+
return { added: acc.added + d.added, removed: acc.removed + d.removed };
|
|
219
|
+
}, { added: 0, removed: 0 })
|
|
220
|
+
: lineDelta(oldText, newText);
|
|
185
221
|
turnFileChanges.push({
|
|
186
|
-
path:
|
|
222
|
+
path: filePath,
|
|
187
223
|
kind: tc.name === 'write_file' && preWriteOld == null ? 'A' : 'M',
|
|
188
|
-
...
|
|
224
|
+
...delta,
|
|
189
225
|
});
|
|
190
226
|
}
|
|
191
227
|
const preview = diff ? '' : summarizeToolResult(tc.name, output);
|
|
@@ -406,7 +442,7 @@ runtimeOrContext = defaultRuntime) {
|
|
|
406
442
|
onToolStart: (name) => spinner.start(t('agent.executing', { tool: name })),
|
|
407
443
|
onToolOutput: (tc, chunk) => batch.recordOutput(tc.id, chunk, layout),
|
|
408
444
|
onToolDone: () => spinner.stop(),
|
|
409
|
-
onToolResult: (tc, output, parsed, preWriteOld, editStartLine) => writeToolResult(tc, output, parsed, preWriteOld, editStartLine, runtime.context.toolRuntime),
|
|
445
|
+
onToolResult: (tc, output, parsed, preWriteOld, editStartLine, multiHunks) => writeToolResult(tc, output, parsed, preWriteOld, editStartLine, runtime.context.toolRuntime, multiHunks),
|
|
410
446
|
onToolBatchEnd: () => {
|
|
411
447
|
// 一次工具轮次结束不再切 UI batch;下一轮若仍无正文,继续复用 currentBatchId。
|
|
412
448
|
},
|
|
@@ -290,7 +290,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
290
290
|
break;
|
|
291
291
|
}
|
|
292
292
|
case 'result':
|
|
293
|
-
hooks.onToolResult?.(event.call, event.outcome.output, event.parsed, event.diff.preWriteOld, event.diff.editStartLine);
|
|
293
|
+
hooks.onToolResult?.(event.call, event.outcome.output, event.parsed, event.diff.preWriteOld, event.diff.editStartLine, event.diff.multiHunks);
|
|
294
294
|
if (event.call.name === 'ask_human' && event.outcome.status === 'success') {
|
|
295
295
|
askHumanCountThisTurn += 1;
|
|
296
296
|
emitTrace('ask_human_call', {
|
|
@@ -326,6 +326,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
326
326
|
delegation: delegationForOrchestrator,
|
|
327
327
|
readDedup,
|
|
328
328
|
argumentErrorHint: (name) => argumentErrorHint(name, runtimeContextState),
|
|
329
|
+
...(result.finishReason ? { finishReason: result.finishReason } : {}),
|
|
329
330
|
onToolOutput: (call, chunk) => hooks.onToolOutput?.(call, chunk),
|
|
330
331
|
...(opts.toolPolicy
|
|
331
332
|
? {
|
|
@@ -401,6 +402,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
401
402
|
allowedToolNames: currentAllowedToolNames(),
|
|
402
403
|
delegation: delegationForOrchestrator(),
|
|
403
404
|
readDedup,
|
|
405
|
+
...(result.finishReason ? { finishReason: result.finishReason } : {}),
|
|
404
406
|
});
|
|
405
407
|
}
|
|
406
408
|
}
|
|
@@ -572,6 +574,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
572
574
|
allowedToolNames: currentAllowedToolNames(),
|
|
573
575
|
delegation: delegationForOrchestrator(),
|
|
574
576
|
readDedup,
|
|
577
|
+
...(result.finishReason ? { finishReason: result.finishReason } : {}),
|
|
575
578
|
}));
|
|
576
579
|
// 完成即发:usage/onToolOutcome/trace 在各 outcome 落地瞬间发出(1s 的
|
|
577
580
|
// grep 不再被 60s 的 web_fetch 卡住可见性);history 回灌严格按原序。
|
|
@@ -671,6 +674,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
671
674
|
delegation: delegationForOrchestrator(),
|
|
672
675
|
readDedup,
|
|
673
676
|
...(hint ? { argumentErrorHint: hint } : {}),
|
|
677
|
+
...(result.finishReason ? { finishReason: result.finishReason } : {}),
|
|
674
678
|
onLockAcquired: (lockedArgs) => {
|
|
675
679
|
entry.diff = readDiffContext(entry.tc, lockedArgs, ctx.jailResolve);
|
|
676
680
|
},
|
|
@@ -685,7 +689,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
685
689
|
for (let offset = 0; offset < entries.length; offset++) {
|
|
686
690
|
const entry = entries[offset];
|
|
687
691
|
const outcome = await outcomes[offset];
|
|
688
|
-
hooks.onToolResult?.(entry.tc, outcome.output, entry.denied ? null : entry.parsed, entry.diff.preWriteOld, entry.diff.editStartLine);
|
|
692
|
+
hooks.onToolResult?.(entry.tc, outcome.output, entry.denied ? null : entry.parsed, entry.diff.preWriteOld, entry.diff.editStartLine, entry.diff.multiHunks);
|
|
689
693
|
pushToolResult(history, entry.tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
690
694
|
const invalidatedFiles = [
|
|
691
695
|
...new Set([...(outcome.changedFiles ?? []), ...(outcome.staleFiles ?? [])]),
|
|
@@ -760,6 +764,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
760
764
|
delegation: delegationForOrchestrator(),
|
|
761
765
|
readDedup,
|
|
762
766
|
...(hint ? { argumentErrorHint: hint } : {}),
|
|
767
|
+
...(result.finishReason ? { finishReason: result.finishReason } : {}),
|
|
763
768
|
onLockAcquired: (lockedArgs) => {
|
|
764
769
|
entry.diff = readDiffContext(entry.tc, lockedArgs, ctx.jailResolve);
|
|
765
770
|
},
|
|
@@ -776,7 +781,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
776
781
|
for (let k = 0; k < entries.length; k++) {
|
|
777
782
|
const entry = entries[k];
|
|
778
783
|
const outcome = await started[k];
|
|
779
|
-
hooks.onToolResult?.(entry.tc, outcome.output, entry.denied ? null : entry.parsed, entry.diff.preWriteOld, entry.diff.editStartLine);
|
|
784
|
+
hooks.onToolResult?.(entry.tc, outcome.output, entry.denied ? null : entry.parsed, entry.diff.preWriteOld, entry.diff.editStartLine, entry.diff.multiHunks);
|
|
780
785
|
pushToolResult(history, entry.tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
781
786
|
const invalidatedFiles = [
|
|
782
787
|
...new Set([...(outcome.changedFiles ?? []), ...(outcome.staleFiles ?? [])]),
|
|
@@ -856,6 +861,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
856
861
|
delegation: delegationForOrchestrator(),
|
|
857
862
|
readDedup,
|
|
858
863
|
...(serialHint ? { argumentErrorHint: serialHint } : {}),
|
|
864
|
+
...(result.finishReason ? { finishReason: result.finishReason } : {}),
|
|
859
865
|
onLockAcquired: (lockedArgs) => {
|
|
860
866
|
if (mutationParsed)
|
|
861
867
|
diff = readDiffContext(tc, lockedArgs, ctx.jailResolve);
|
|
@@ -866,7 +872,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
866
872
|
traceToolEnd(tc, i, outcome);
|
|
867
873
|
const output = outcome.output;
|
|
868
874
|
hooks.onToolDone?.();
|
|
869
|
-
hooks.onToolResult?.(tc, output, mutationParsed, diff.preWriteOld, diff.editStartLine);
|
|
875
|
+
hooks.onToolResult?.(tc, output, mutationParsed, diff.preWriteOld, diff.editStartLine, diff.multiHunks);
|
|
870
876
|
if (tc.name === 'ask_human' && outcome.status === 'success') {
|
|
871
877
|
askHumanCountThisTurn += 1;
|
|
872
878
|
emitTrace('ask_human_call', {
|
|
@@ -49,6 +49,7 @@ class LegacyCompatibleToolDispatcher {
|
|
|
49
49
|
delegation: request.delegation(),
|
|
50
50
|
readDedup: request.readDedup,
|
|
51
51
|
...(hint ? { argumentErrorHint: hint } : {}),
|
|
52
|
+
...(request.finishReason ? { finishReason: request.finishReason } : {}),
|
|
52
53
|
...(onLockAcquired ? { onLockAcquired } : {}),
|
|
53
54
|
});
|
|
54
55
|
const executionEvents = (index, parsed, outcome) => {
|
|
@@ -13,6 +13,8 @@ import { defaultToolRuntime } from '../tools/registry.js';
|
|
|
13
13
|
import { jailResolve } from '../sandbox/index.js';
|
|
14
14
|
import { contextState } from '../session/compact.js';
|
|
15
15
|
import { capToolResultForHistory } from '../session/compact.js';
|
|
16
|
+
import { getActiveSessionStore } from '../session/store.js';
|
|
17
|
+
import { getCurrentSessionId } from '../session/state.js';
|
|
16
18
|
import { recordArtifact, knownEditTargets } from '../context/index.js';
|
|
17
19
|
import { isToolResultSuccess } from '../context/utils.js';
|
|
18
20
|
import { appendGuiAction } from '../session/gui-actions.js';
|
|
@@ -126,31 +128,59 @@ export function unregisteredToolOutcome(name, toolRuntime) {
|
|
|
126
128
|
/** mutation 执行前读旧内容供 diff:write_file 取整文件旧内容(不存在→null=新建),
|
|
127
129
|
* edit_file 取 old_string 起始行号(供 diff 显示真实文件行号)。读不到则 diff 退化为相对行号。
|
|
128
130
|
* 非 mutation 或参数非法返 { preWriteOld: null, editStartLine: 1 }。失败不阻断。 */
|
|
131
|
+
const toLf = (s) => s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
129
132
|
export function readDiffContext(tc, parsed, resolvePath = jailResolve) {
|
|
133
|
+
const empty = { preWriteOld: null, editStartLine: 1 };
|
|
130
134
|
if (!parsed)
|
|
131
|
-
return
|
|
135
|
+
return empty;
|
|
132
136
|
const p = String(parsed.path ?? '');
|
|
133
137
|
if (!p)
|
|
134
|
-
return
|
|
138
|
+
return empty;
|
|
135
139
|
if (tc.name === 'write_file') {
|
|
136
140
|
try {
|
|
137
141
|
// jailResolve:沙箱越界(../../、绝对外圈、软链出圈)抛错 → catch 兜底返 null,不泄露牢外内容(TOCTOU)
|
|
138
142
|
return { preWriteOld: readFileSync(resolvePath(p), 'utf8'), editStartLine: 1 };
|
|
139
143
|
}
|
|
140
144
|
catch {
|
|
141
|
-
return
|
|
145
|
+
return empty; // 文件不存在(新建)、不可读 或 沙箱越界(不泄露)
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
if (tc.name === 'edit_file') {
|
|
145
149
|
// 行尾归一化:LLM 生成的 old_string 用 LF(\n),但 Windows 文件可能是 CRLF(\r\n),
|
|
146
150
|
// 不统一则 indexOf 必败、editStartLine 恒为 1。与 edit-file.ts 保持一致归一化为 LF。
|
|
147
|
-
const
|
|
148
|
-
.replace(/\r\n/g, '\n')
|
|
149
|
-
.replace(/\r/g, '\n');
|
|
151
|
+
const replacements = Array.isArray(parsed.replacements) ? parsed.replacements : null;
|
|
150
152
|
try {
|
|
151
153
|
// jailResolve:同上,沙箱越界抛错 → catch 兜底,不泄露牢外内容
|
|
152
154
|
const raw = readFileSync(resolvePath(p), 'utf8');
|
|
153
|
-
const data = raw
|
|
155
|
+
const data = toLf(raw);
|
|
156
|
+
// 多替换模式:在「编辑前」的原文上为每个 old_string 定位唯一命中的起始行,
|
|
157
|
+
// 供多 hunk diff 显示真实文件行号。任一项非唯一/找不到就不带 hunk(退回工具结果摘要),
|
|
158
|
+
// 真正的原子校验仍以 edit-file.ts execute 为准,这里只做展示、失败不阻断。
|
|
159
|
+
if (replacements) {
|
|
160
|
+
const hunks = [];
|
|
161
|
+
for (const item of replacements) {
|
|
162
|
+
if (!item || typeof item !== 'object')
|
|
163
|
+
return empty;
|
|
164
|
+
const rec = item;
|
|
165
|
+
if (typeof rec.old_string !== 'string' || typeof rec.new_string !== 'string')
|
|
166
|
+
return empty;
|
|
167
|
+
const oldStr = toLf(rec.old_string);
|
|
168
|
+
if (!oldStr)
|
|
169
|
+
return empty;
|
|
170
|
+
const first = data.indexOf(oldStr);
|
|
171
|
+
if (first < 0 || data.indexOf(oldStr, first + oldStr.length) !== -1)
|
|
172
|
+
return empty;
|
|
173
|
+
hunks.push({
|
|
174
|
+
oldStr: rec.old_string,
|
|
175
|
+
newStr: rec.new_string,
|
|
176
|
+
startLine: data.slice(0, first).split('\n').length,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (hunks.length === 0)
|
|
180
|
+
return empty;
|
|
181
|
+
return { preWriteOld: null, editStartLine: hunks[0].startLine, multiHunks: hunks };
|
|
182
|
+
}
|
|
183
|
+
const oldStr = toLf(String(parsed.old_string ?? ''));
|
|
154
184
|
const idx = oldStr ? data.indexOf(oldStr) : -1;
|
|
155
185
|
return {
|
|
156
186
|
preWriteOld: null,
|
|
@@ -158,10 +188,10 @@ export function readDiffContext(tc, parsed, resolvePath = jailResolve) {
|
|
|
158
188
|
};
|
|
159
189
|
}
|
|
160
190
|
catch {
|
|
161
|
-
return
|
|
191
|
+
return empty; // 读不到:diff 退化为相对行号(含沙箱越界)
|
|
162
192
|
}
|
|
163
193
|
}
|
|
164
|
-
return
|
|
194
|
+
return empty;
|
|
165
195
|
}
|
|
166
196
|
/** 回灌 tool 结果到 history。
|
|
167
197
|
* 正常路径只做单条 hard cap;原始 output 同时供 TUI 展示,因此用户与模型
|
|
@@ -169,12 +199,27 @@ export function readDiffContext(tc, parsed, resolvePath = jailResolve) {
|
|
|
169
199
|
* 不在这里改写旧正文;所有自动清理与压缩统一由 80% pressure scheduler 决定。 */
|
|
170
200
|
export function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runtimeContextState = contextState, succeededOverride) {
|
|
171
201
|
const succeeded = succeededOverride ?? isToolResultSuccess(output);
|
|
202
|
+
const capped = capToolResultForHistory(tc.name, output);
|
|
203
|
+
// push-time 截断兜底:完整结果另存 compacted/result-<id>.txt,并在进 history 的
|
|
204
|
+
// 截断内容尾部留一行全文指针。仅主 agent(共享 contextState)落盘——子 agent 的
|
|
205
|
+
// 结果随其临时 history 消亡,不污染主会话。best-effort:存不上就只保留截断内容。
|
|
206
|
+
let content = capped;
|
|
207
|
+
if (capped.length < output.length && runtimeContextState === contextState) {
|
|
208
|
+
try {
|
|
209
|
+
const rel = getActiveSessionStore().archiveToolResult(getCurrentSessionId() ?? '', tc.id, output);
|
|
210
|
+
if (rel)
|
|
211
|
+
content = `${capped.replace(/\s+$/, '')}\n…[完整结果已归档,需要原文时 grep/read_file:${rel}]`;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// 归档失败不影响工具回灌。
|
|
215
|
+
}
|
|
216
|
+
}
|
|
172
217
|
const msg = {
|
|
173
218
|
role: 'tool',
|
|
174
219
|
tool_call_id: tc.id,
|
|
175
220
|
// Preserve evidence verbatim in normal operation; the hard per-result cap
|
|
176
221
|
// remains solely as a request-size safety rail.
|
|
177
|
-
content
|
|
222
|
+
content,
|
|
178
223
|
};
|
|
179
224
|
history.push(msg);
|
|
180
225
|
const messageIndex = history.length - 1;
|
package/dist/config/file.js
CHANGED
|
@@ -22,11 +22,19 @@ export function readConfigFile() {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* 读-合并-写:把 entries 覆盖进 ~/.mocode/config,保留文件里其它键。
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* 值为 null 表示从配置文件删除该键(用于切到「不带某可选值」的预设时清除残留,
|
|
26
|
+
* 否则旧值会一直盖过新预设)。mkdirSync 兜底(首跑无 ~/.mocode)。不吞 I/O 异常——
|
|
27
|
+
* 向导路径让其抛(与历史行为一致);UI 偏好路径用 `updateConfigKey`(自带 try/catch 静默)。
|
|
27
28
|
*/
|
|
28
29
|
export function writeConfigKeys(entries) {
|
|
29
|
-
const
|
|
30
|
+
const existing = readConfigFile();
|
|
31
|
+
const merged = { ...existing };
|
|
32
|
+
for (const [k, v] of Object.entries(entries)) {
|
|
33
|
+
if (v === null)
|
|
34
|
+
delete merged[k];
|
|
35
|
+
else
|
|
36
|
+
merged[k] = v;
|
|
37
|
+
}
|
|
30
38
|
const body = Object.entries(merged)
|
|
31
39
|
.map(([k, v]) => `${k}=${v}`)
|
|
32
40
|
.join('\n');
|
package/dist/config/index.js
CHANGED
|
@@ -57,6 +57,7 @@ const LLM_ENV_KEYS = [
|
|
|
57
57
|
'LLM_API_KEY',
|
|
58
58
|
'LLM_MODEL',
|
|
59
59
|
'CONTEXT_WINDOW_TOKENS',
|
|
60
|
+
'MAX_TOKENS',
|
|
60
61
|
'ANTHROPIC_PROMPT_CACHE',
|
|
61
62
|
'REASONING_EFFORT',
|
|
62
63
|
];
|
|
@@ -485,7 +486,7 @@ ${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
|
485
486
|
- Silent Execution: invoke tools directly without preamble. Output visible text ONLY for the final answer and critical mid-task findings. Strictly no step-by-step narration (no "let me…", "让我先…", "now checking…" between calls).
|
|
486
487
|
- Go directly to a known path or symbol; use discovery tools only when the location is unknown.
|
|
487
488
|
- Edit against a FRESH read: before editing, call read_file on the exact path and copy the exact target text. Never reconstruct old_string from a grep/summary/diff — those lose whitespace and indentation and cause edit failures.
|
|
488
|
-
- Pick the right file tool:
|
|
489
|
+
- Pick the right file tool: for an EXISTING file, DEFAULT to edit_file — one old_string for a single change, replacements[] for many separate changes in one atomic call, or a line range for a big block. Do NOT overwrite an existing file with write_file just to tweak parts of it (re-sending the whole file wastes tokens and its long JSON is truncation-prone). Use write_file only to create a new file, to append (append=true needs no read_file and no hash), or to rewrite essentially the WHOLE file (a full rewrite needs the latest read_file hash). expected_hash is OPTIONAL in edit_file old_string/replacements mode, REQUIRED in line-range mode. Never write a throwaway script (tmp_patch*.cjs, fix_*.py, ...) to modify source files: scripts run outside ChangeSet tracking, so rollback and diff cannot see them.
|
|
489
490
|
- A read_file hash from before a compaction, session resume, edit conflict, or external change is STALE and will be rejected if you pass it. In edit_file old_string mode just omit expected_hash; otherwise re-read rather than reuse an old hash.
|
|
490
491
|
- Emit multiple independent tool calls in ONE assistant message so they run concurrently — e.g. several read_file regions, a grep plus a glob, or several web_fetch calls. One lookup per message wastes a full model round-trip each time. Place parallel-safe calls consecutively; keep any call that depends on their results (e.g. an edit) for the next message.
|
|
491
492
|
- Never batch a read with an edit that depends on it; do not repeat overlapping reads or unchanged failed calls.
|
|
@@ -518,7 +519,10 @@ ${buildVoiceSection()}
|
|
|
518
519
|
dynamicParts.push(`## Session state (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
|
|
519
520
|
'For tasks with 3+ steps or context-loss risk, keep an execution plan with `plan_update`; record non-obvious findings/decisions/open questions/risks with `note_append`. ' +
|
|
520
521
|
'Both write notes.md, which survives compaction — run read_file on it to recover context after compaction. ' +
|
|
521
|
-
'Their formats and rules live in each tool\u2019s description; do not hand-edit the files.'
|
|
522
|
+
'Their formats and rules live in each tool\u2019s description; do not hand-edit the files. ' +
|
|
523
|
+
'When compaction drops older conversation, its verbatim text (user messages, your earlier decisions, full tool args/results) is archived under ' +
|
|
524
|
+
`\`.mocode/sessions/${sessionId ?? '<id>'}/compacted/\` (see index.md there): the summary line ends with an [archived: …] pointer. ` +
|
|
525
|
+
'Before re-running a command or guessing what an old output said, grep that directory with glob narrowed to `.mocode/sessions/**` (required: .mocode is gitignored), then read_file the segment at the hit lines.');
|
|
522
526
|
// 日期段:模型需要知道今天才能判断 freshness(web 搜索、版本时效)。只随天变化,
|
|
523
527
|
// 不破坏会话内前缀缓存;置于 '## Session state' 之前,子 agent 切片仍保留(无害且有用)。
|
|
524
528
|
const todaySection = buildTodaySection();
|
|
@@ -578,7 +582,8 @@ export const config = {
|
|
|
578
582
|
model: llmKeysFromShell.includes('LLM_MODEL')
|
|
579
583
|
? process.env.LLM_MODEL || 'gpt-4o-mini'
|
|
580
584
|
: (__activePreset?.model ?? process.env.LLM_MODEL ?? 'gpt-4o-mini'),
|
|
581
|
-
|
|
585
|
+
// 优先级:shell env > 激活预设(目录添加时按 limit.output 预填)> undefined=不下发吃后端默认。
|
|
586
|
+
maxTokens: process.env.MAX_TOKENS ? Number(process.env.MAX_TOKENS) : (__activePreset?.maxTokens ?? undefined),
|
|
582
587
|
// 用 getter 而非 buildBasePrompt() 立即求值:对象字面量初始化期间 buildBasePrompt 会读取
|
|
583
588
|
// config.model 等字段,直接调用会触发 TDZ。Getter 也让语言、persona 等运行时变化在下一轮生效。
|
|
584
589
|
get systemPrompt() {
|
|
@@ -724,6 +729,14 @@ export function updateModelConfig(opts) {
|
|
|
724
729
|
config.contextWindowTokens = opts.contextWindowTokens;
|
|
725
730
|
process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
|
|
726
731
|
}
|
|
732
|
+
if (opts.maxTokens !== undefined) {
|
|
733
|
+
// null = 显式清除(切到无该值的预设);数字 = 设置。同步 env 保证本窗口立即按新值/不发该字段。
|
|
734
|
+
config.maxTokens = opts.maxTokens === null ? undefined : opts.maxTokens;
|
|
735
|
+
if (opts.maxTokens === null)
|
|
736
|
+
delete process.env.MAX_TOKENS;
|
|
737
|
+
else
|
|
738
|
+
process.env.MAX_TOKENS = String(opts.maxTokens);
|
|
739
|
+
}
|
|
727
740
|
if (opts.anthropicPromptCache !== undefined) {
|
|
728
741
|
config.anthropicPromptCache = opts.anthropicPromptCache;
|
|
729
742
|
process.env.ANTHROPIC_PROMPT_CACHE = opts.anthropicPromptCache ? 'true' : 'false';
|
package/dist/config/presets.js
CHANGED
|
@@ -88,6 +88,14 @@ export function parsePreset(raw) {
|
|
|
88
88
|
if (typeof contextWindow !== 'number' || !Number.isFinite(contextWindow) || contextWindow <= 0) {
|
|
89
89
|
throw new Error(`预设 ${name}: contextWindow 必须为正数`);
|
|
90
90
|
}
|
|
91
|
+
// maxTokens 可选:缺省=不下发(吃后端默认);给了就必须是正整数。
|
|
92
|
+
let maxTokens;
|
|
93
|
+
if (obj.maxTokens !== undefined && obj.maxTokens !== null) {
|
|
94
|
+
if (typeof obj.maxTokens !== 'number' || !Number.isFinite(obj.maxTokens) || obj.maxTokens <= 0) {
|
|
95
|
+
throw new Error(`预设 ${name}: maxTokens 必须为正数`);
|
|
96
|
+
}
|
|
97
|
+
maxTokens = Math.floor(obj.maxTokens);
|
|
98
|
+
}
|
|
91
99
|
const provider = normalizePresetProvider(obj.provider);
|
|
92
100
|
const anthropicPromptCache = provider === 'anthropic' && obj.anthropicPromptCache !== false;
|
|
93
101
|
// reasoningEffort 缺省 = auto;显式写了非法值按现有校验风格报错(坏文件由 listPresets 跳过)。
|
|
@@ -132,6 +140,7 @@ export function parsePreset(raw) {
|
|
|
132
140
|
apiKey,
|
|
133
141
|
model,
|
|
134
142
|
contextWindow: Math.floor(contextWindow),
|
|
143
|
+
...(maxTokens ? { maxTokens } : {}),
|
|
135
144
|
anthropicPromptCache,
|
|
136
145
|
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
137
146
|
...extra,
|
package/dist/headless.js
CHANGED
|
@@ -23,6 +23,8 @@ import { initializeAllMcp, getMcpTools, closeAllMcp } from './mcp/index.js';
|
|
|
23
23
|
import { config, buildBasePrompt, isMemoryEnabled } from './config/index.js';
|
|
24
24
|
import { effectiveSystemPrompt } from './skills/index.js';
|
|
25
25
|
import { buildMemoryIndexSection } from './memory/store.js';
|
|
26
|
+
import { buildAboutYouSection, initPersona } from './memory/persona.js';
|
|
27
|
+
import { buildProjectGraphHint } from './memory/recall.js';
|
|
26
28
|
import { defaultRuntime, Runtime } from './runtime/index.js';
|
|
27
29
|
import { runAgentCore } from './agent/core.js';
|
|
28
30
|
import { summarizeToolCall, summarizeToolResult } from './ui/render.js';
|
|
@@ -154,7 +156,12 @@ export async function runHeadless(opts) {
|
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
// 系统提示:Bot 岗位角色在前,基础约定在后;白名单存在时补一句工具边界。
|
|
157
|
-
const
|
|
159
|
+
const memoryOn = isMemoryEnabled();
|
|
160
|
+
initPersona(memoryOn);
|
|
161
|
+
const baseConventions = buildBasePrompt(sessionId) +
|
|
162
|
+
buildAboutYouSection(memoryOn) +
|
|
163
|
+
buildMemoryIndexSection(memoryOn) +
|
|
164
|
+
buildProjectGraphHint(memoryOn);
|
|
158
165
|
const systemContent = botInfo
|
|
159
166
|
? `# Role\n${botInfo.bot.systemPrompt}\n\n` +
|
|
160
167
|
`${botInfo.bot.description ? `**Position:** ${botInfo.bot.description}\n\n` : ''}` +
|
package/dist/index.js
CHANGED
|
@@ -252,7 +252,7 @@ async function main() {
|
|
|
252
252
|
}
|
|
253
253
|
process.exit(0);
|
|
254
254
|
}
|
|
255
|
-
const loaded = loadSession(id);
|
|
255
|
+
const loaded = loadSession(id, { restoreAssets: true });
|
|
256
256
|
if (!loaded || !loaded.history.length) {
|
|
257
257
|
console.error(t('cli.sessionMissing', { id }));
|
|
258
258
|
process.exit(1);
|
package/dist/llm/index.js
CHANGED
|
@@ -740,6 +740,10 @@ async function chatOnce(messages, handlers, signal, toolsOverride, runtime = def
|
|
|
740
740
|
let consumedAny = false;
|
|
741
741
|
const thinkFilter = new ThinkTagFilter();
|
|
742
742
|
let usage;
|
|
743
|
+
// finish_reason 只在最后一个带 choice 的 chunk 出现;取最后一次非空值。
|
|
744
|
+
// 'length' 意味着输出在 max_tokens 处被硬切断——长 tool_call 参数此时必为残缺 JSON,
|
|
745
|
+
// 工具层据此把笼统的 INVALID_JSON 升级成截断恢复指引。
|
|
746
|
+
let finishReason;
|
|
743
747
|
const toolAcc = new Map();
|
|
744
748
|
const emitVisible = (text) => {
|
|
745
749
|
if (!text)
|
|
@@ -784,7 +788,11 @@ async function chatOnce(messages, handlers, signal, toolsOverride, runtime = def
|
|
|
784
788
|
cachedTokens: usage.cachedTokens,
|
|
785
789
|
});
|
|
786
790
|
}
|
|
787
|
-
const
|
|
791
|
+
const choice0 = chunk.choices?.[0];
|
|
792
|
+
const delta = choice0?.delta;
|
|
793
|
+
if (typeof choice0?.finish_reason === 'string' && choice0.finish_reason) {
|
|
794
|
+
finishReason = choice0.finish_reason;
|
|
795
|
+
}
|
|
788
796
|
if (!delta)
|
|
789
797
|
continue; // 末尾 usage-only chunk 等无 delta
|
|
790
798
|
if (delta.content) {
|
|
@@ -835,6 +843,7 @@ async function chatOnce(messages, handlers, signal, toolsOverride, runtime = def
|
|
|
835
843
|
content: consumedAny ? visibleContent : null,
|
|
836
844
|
toolCalls,
|
|
837
845
|
usage,
|
|
846
|
+
finishReason,
|
|
838
847
|
};
|
|
839
848
|
}
|
|
840
849
|
// ── token 估算(自包含,不依赖 ui / 外部 tokenizer)──────────────────────
|
|
@@ -315,6 +315,8 @@ export async function anthropicChatOnce(messages, handlers, signal, tools, runti
|
|
|
315
315
|
await throwResponseError(response);
|
|
316
316
|
let content = '';
|
|
317
317
|
let usage;
|
|
318
|
+
// message_delta 的 delta.stop_reason 给出停止原因;max_tokens 归一到 OpenAI 的 'length'。
|
|
319
|
+
let finishReason;
|
|
318
320
|
const toolAcc = new Map();
|
|
319
321
|
const live = { cjk: 0, other: 0 };
|
|
320
322
|
const reportLive = () => {
|
|
@@ -391,6 +393,12 @@ export async function anthropicChatOnce(messages, handlers, signal, tools, runti
|
|
|
391
393
|
continue;
|
|
392
394
|
}
|
|
393
395
|
if (type === 'message_delta') {
|
|
396
|
+
// stop_reason 位于 message_delta 的 delta 子对象(与 usage 同级事件)。
|
|
397
|
+
const d = event.delta;
|
|
398
|
+
if (typeof d?.stop_reason === 'string' && d.stop_reason) {
|
|
399
|
+
// Anthropic: end_turn / max_tokens / stop_sequence / tool_use / tool_use 等。
|
|
400
|
+
finishReason = d.stop_reason === 'max_tokens' ? 'length' : d.stop_reason;
|
|
401
|
+
}
|
|
394
402
|
const raw = event.usage;
|
|
395
403
|
if (raw) {
|
|
396
404
|
const previous = usage ?? usageFromAnthropic({});
|
|
@@ -414,5 +422,5 @@ export async function anthropicChatOnce(messages, handlers, signal, tools, runti
|
|
|
414
422
|
const toolCalls = [...toolAcc.entries()]
|
|
415
423
|
.sort((a, b) => a[0] - b[0])
|
|
416
424
|
.map(([, call]) => ({ ...call, arguments: call.arguments || '{}' }));
|
|
417
|
-
return { content: content || null, toolCalls, usage };
|
|
425
|
+
return { content: content || null, toolCalls, usage, finishReason };
|
|
418
426
|
}
|
|
@@ -407,5 +407,11 @@ export async function vertexChatOnce(messages, handlers, signal, tools, runtime,
|
|
|
407
407
|
content += marker;
|
|
408
408
|
handlers.onText?.(marker);
|
|
409
409
|
}
|
|
410
|
-
return {
|
|
410
|
+
return {
|
|
411
|
+
content: content || null,
|
|
412
|
+
toolCalls,
|
|
413
|
+
usage,
|
|
414
|
+
// Gemini MAX_TOKENS 归一到 OpenAI 的 'length',供工具层给出截断恢复指引。
|
|
415
|
+
...(finishReason ? { finishReason: finishReason === 'MAX_TOKENS' ? 'length' : finishReason } : {}),
|
|
416
|
+
};
|
|
411
417
|
}
|