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/memory/reflect.js
CHANGED
|
@@ -13,7 +13,8 @@ import path from 'node:path';
|
|
|
13
13
|
import { chat } from '../llm/index.js';
|
|
14
14
|
import { config, isMemoryEnabled } from '../config/index.js';
|
|
15
15
|
import { saveEntry, updateEntry, forgetEntry, loadAll, gcMemories } from './store.js';
|
|
16
|
-
import { addTriple } from './graph.js';
|
|
16
|
+
import { addTriple, ensurePrimaryPerson, PRIMARY_PERSON_ID } from './graph.js';
|
|
17
|
+
import { normalizeRelation, isKnownRelation, relationCheatSheet } from './relation-vocab.js';
|
|
17
18
|
// ── 日志(静默容错,裁尾保最近)─────────────────────────────────────────────
|
|
18
19
|
function logPath() {
|
|
19
20
|
return path.join(process.cwd(), '.mocode', 'memory.log');
|
|
@@ -91,7 +92,7 @@ function buildMemorySample() {
|
|
|
91
92
|
}
|
|
92
93
|
const TYPES = 'decision | fact | pitfall | reference | feedback';
|
|
93
94
|
const REFLECT_SYS = `You are mocode's memory reflector. Review the recent session and existing memories, producing **only** updates worth remembering long-term.
|
|
94
|
-
Output strictly JSON (no markdown code blocks, no explanatory text): {"saves":[{"type":"...","name":"...","summary":"...","body":"..."}],"updates":[{"id":"...","reason":"...","summary":"...","body":"..."}],"forgets":[{"id":"...","reason":"..."}],"triples":[{"src":"...","relation":"...","dst":"...","fact":"..."}]}
|
|
95
|
+
Output strictly JSON (no markdown code blocks, no explanatory text): {"saves":[{"type":"...","name":"...","summary":"...","body":"..."}],"updates":[{"id":"...","reason":"...","summary":"...","body":"..."}],"forgets":[{"id":"...","reason":"..."}],"triples":[{"src":"...","relation":"...","dst":"...","fact":"..."}],"profile":[{"relation":"...","dst":"...","fact":"..."}]}
|
|
95
96
|
Empty arrays are valid (if nothing is worth saving, all arrays are empty).
|
|
96
97
|
Rules:
|
|
97
98
|
① Only store non-obvious, cross-session-useful facts/decisions/pitfalls; do not store current bugs, temp files, undecided TODOs, or volatile items;
|
|
@@ -100,16 +101,17 @@ Rules:
|
|
|
100
101
|
④ names in saves must be concise and not collide with existing ones; type ∈ {${TYPES}};
|
|
101
102
|
⑤ If an existing memory contradicts new facts or is outdated, update the old entry (modify summary/body) rather than creating a duplicate;
|
|
102
103
|
⑥ forgets are for memories clearly stale / superseded by a new entry (archive, not hard-delete);
|
|
103
|
-
⑦ triples are knowledge-graph facts distilled from this session: concise entity names (lowercase snake_case or proper nouns)
|
|
104
|
+
⑦ triples are PROJECT knowledge-graph facts about THIS CODEBASE distilled from this session: concise entity names (lowercase snake_case or proper nouns) plus a one-line fact. Prioritize the high-reuse structural relations in this order — dependency/ownership (depends_on, uses, used_by, calls, part_of), location (implemented_in, reads, writes, configured_by, runs_on, listens_on), verification (tests, documented_in), rationale/risk (decided_by, conflicts_with); full list: ${relationCheatSheet('project')}. Favor facts that would help a future session navigate or avoid re-deriving (which module implements X, what depends on Y, what conflicts with Z). A relation outside this list is allowed only when none fits (it gets flagged for review). Only emit triples that are stable, non-obvious and reusable in this repo (2-6 at most).
|
|
105
|
+
⑧ profile is for facts about THE USER (the human), which must apply across ALL projects — identity, role, expertise, language/communication style, stable preferences, current goals, constraints. They are attached to the single canonical person entity. Each item: dst is a short atomic value/entity, fact is one neutral Chinese sentence stating it. Prefer these relations: ${relationCheatSheet('global')}. A relation outside this list is allowed only when none fits. NEVER put repo paths, commands, file names, ports, secrets or one-task details in profile — those belong in triples. Only emit profile when the user explicitly revealed something durable about themselves (0-3 items); when unsure, omit.`;
|
|
104
106
|
const REFLECT_USER = (transcript, sample) => `## Recent session\n${transcript}\n\n## Existing memories\n${sample}\n\nProduce JSON:`;
|
|
105
|
-
function parsePlan(content) {
|
|
107
|
+
export function parsePlan(content) {
|
|
106
108
|
if (!content)
|
|
107
109
|
return null;
|
|
108
110
|
let s = content.trim();
|
|
109
|
-
// 去 ```json … ``` 代码块(模型偶发包裹)
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
s =
|
|
111
|
+
// 去 ```json … ``` 代码块(模型偶发包裹);取最后一个块(前面可能有解释文字)。
|
|
112
|
+
const fences = [...s.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)];
|
|
113
|
+
if (fences.length)
|
|
114
|
+
s = fences[fences.length - 1][1].trim();
|
|
113
115
|
// 容错:截到首个 { 到末个 }
|
|
114
116
|
const first = s.indexOf('{');
|
|
115
117
|
const last = s.lastIndexOf('}');
|
|
@@ -122,12 +124,40 @@ function parsePlan(content) {
|
|
|
122
124
|
return p;
|
|
123
125
|
}
|
|
124
126
|
catch {
|
|
127
|
+
// 再试一次:去掉对象/数组尾逗号(模型常见的非法 JSON)。
|
|
128
|
+
try {
|
|
129
|
+
const p = JSON.parse(s.replace(/,\s*([}\]])/g, '$1'));
|
|
130
|
+
if (p && typeof p === 'object')
|
|
131
|
+
return p;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// 落到 null
|
|
135
|
+
}
|
|
125
136
|
return null;
|
|
126
137
|
}
|
|
127
138
|
}
|
|
139
|
+
/** 取一次反思 LLM 返回的可解析文本(content 优先;tool_calls 的 JSON 串参数兜底)。 */
|
|
140
|
+
async function fetchReflection(messages, signal) {
|
|
141
|
+
const r = await chat(messages, {}, signal, undefined, { reasoningEffort: 'low' });
|
|
142
|
+
if (!r.toolCalls.length && r.content)
|
|
143
|
+
return { content: r.content, raw: null };
|
|
144
|
+
// 推理模型偶发只把 JSON 塞进 tool_calls 参数、或 content 为 null:从工具调用里捞 JSON 串。
|
|
145
|
+
for (const tc of r.toolCalls) {
|
|
146
|
+
const args = tc.function?.arguments;
|
|
147
|
+
if (typeof args === 'string' && args.includes('{'))
|
|
148
|
+
return { content: args, raw: null };
|
|
149
|
+
}
|
|
150
|
+
// 无可用正文(典型:只吐 reasoning_content):保留简短指纹供日志排查。
|
|
151
|
+
const fingerprint = r.content
|
|
152
|
+
? `content=${r.content.slice(0, 200)}`
|
|
153
|
+
: `content=null, toolCalls=${r.toolCalls.length}`;
|
|
154
|
+
return { content: null, raw: fingerprint };
|
|
155
|
+
}
|
|
128
156
|
/**
|
|
129
157
|
* 跑一次反思:chat() 空 handlers(静默)→ 解析 JSON → 经 store 落地 → gcMemories。
|
|
130
|
-
*
|
|
158
|
+
* 首轮解析失败时做**一次修复式重试**(明确要求"只输出 JSON、不要思考过程"),
|
|
159
|
+
* 重试仍失败才报 parse-failed——此时 rawParseFailure 带原始返回指纹,由 kickoff 落
|
|
160
|
+
* memory.log,不再像旧版那样无声丢掉一整轮且无从排查。store 调用全同步。
|
|
131
161
|
* 60s 超时(AbortSignal.timeout)防 exit 时 drain 挂死。
|
|
132
162
|
*/
|
|
133
163
|
export async function runReflection(transcript, signal) {
|
|
@@ -135,7 +165,7 @@ export async function runReflection(transcript, signal) {
|
|
|
135
165
|
const sample = buildMemorySample();
|
|
136
166
|
const sys = { role: 'system', content: REFLECT_SYS };
|
|
137
167
|
const user = { role: 'user', content: REFLECT_USER(transcript, sample) };
|
|
138
|
-
|
|
168
|
+
const result = {
|
|
139
169
|
ts,
|
|
140
170
|
saves: 0,
|
|
141
171
|
updates: 0,
|
|
@@ -145,12 +175,27 @@ export async function runReflection(transcript, signal) {
|
|
|
145
175
|
gcCapped: 0,
|
|
146
176
|
gcGced: 0,
|
|
147
177
|
};
|
|
178
|
+
const timeoutSignal = signal ?? AbortSignal.timeout(60000);
|
|
148
179
|
let content = null;
|
|
149
180
|
try {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
const first = await fetchReflection([sys, user], timeoutSignal);
|
|
182
|
+
content = first.content;
|
|
183
|
+
// 首轮拿不到可解析正文 → 一次修复式重试(同一超时信号的剩余预算)。
|
|
184
|
+
if (parsePlan(content) === null) {
|
|
185
|
+
result.repairAttempted = true;
|
|
186
|
+
const repairUser = {
|
|
187
|
+
role: 'user',
|
|
188
|
+
content: '你的上一条回复无法被解析为目标 JSON。现在只输出一个 JSON 对象,不要 markdown 围栏、不要解释、不要思考过程,' +
|
|
189
|
+
'形状严格为 {"saves":[],"updates":[],"forgets":[],"triples":[]};没有值得长期记住的内容就全部给空数组。',
|
|
190
|
+
};
|
|
191
|
+
const second = await fetchReflection([sys, user, { role: 'assistant', content: content ?? '' }, repairUser], timeoutSignal);
|
|
192
|
+
if (second.content)
|
|
193
|
+
content = second.content;
|
|
194
|
+
if (!content && first.raw)
|
|
195
|
+
result.rawParseFailure = first.raw;
|
|
196
|
+
else if (second.raw && parsePlan(content) === null)
|
|
197
|
+
result.rawParseFailure = second.raw;
|
|
198
|
+
}
|
|
154
199
|
}
|
|
155
200
|
catch (e) {
|
|
156
201
|
result.error = e instanceof Error ? e.name + ': ' + e.message : String(e);
|
|
@@ -159,6 +204,8 @@ export async function runReflection(transcript, signal) {
|
|
|
159
204
|
const plan = parsePlan(content);
|
|
160
205
|
if (!plan) {
|
|
161
206
|
result.error = 'parse-failed';
|
|
207
|
+
if (!result.rawParseFailure)
|
|
208
|
+
result.rawParseFailure = (content ?? '').slice(0, 300);
|
|
162
209
|
return result;
|
|
163
210
|
}
|
|
164
211
|
let saves = 0;
|
|
@@ -201,14 +248,19 @@ export async function runReflection(transcript, signal) {
|
|
|
201
248
|
}
|
|
202
249
|
}
|
|
203
250
|
// 知识图谱三元组:容错——单项失败跳过,不影响 saves/updates/forgets 已落地的结果。
|
|
251
|
+
// 关系名先归一(别名/拼写);词表外关系仍允许写(软封闭世界),但计 relViolations 供观测。
|
|
204
252
|
let triples = 0;
|
|
253
|
+
let relViolations = 0;
|
|
205
254
|
if (Array.isArray(plan.triples)) {
|
|
206
255
|
for (const t of plan.triples.slice(0, 10)) {
|
|
207
256
|
if (!t?.src || !t?.relation || !t?.dst)
|
|
208
257
|
continue;
|
|
258
|
+
const relation = normalizeRelation(String(t.relation));
|
|
259
|
+
if (!isKnownRelation(relation, 'project'))
|
|
260
|
+
relViolations++;
|
|
209
261
|
const r = addTriple({
|
|
210
262
|
src: String(t.src),
|
|
211
|
-
relation
|
|
263
|
+
relation,
|
|
212
264
|
dst: String(t.dst),
|
|
213
265
|
fact: t.fact ? String(t.fact) : undefined,
|
|
214
266
|
});
|
|
@@ -216,8 +268,43 @@ export async function runReflection(transcript, signal) {
|
|
|
216
268
|
triples++;
|
|
217
269
|
}
|
|
218
270
|
}
|
|
271
|
+
// 全局用户画像:挂到规范 person-primary、写 global 图(与项目 triples 分流)。
|
|
272
|
+
let profiles = 0;
|
|
273
|
+
if (Array.isArray(plan.profile) && plan.profile.length > 0) {
|
|
274
|
+
let personId = PRIMARY_PERSON_ID;
|
|
275
|
+
try {
|
|
276
|
+
personId = ensurePrimaryPerson().id;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
personId = PRIMARY_PERSON_ID;
|
|
280
|
+
}
|
|
281
|
+
for (const p of plan.profile.slice(0, 6)) {
|
|
282
|
+
if (!p?.relation || !p.dst)
|
|
283
|
+
continue;
|
|
284
|
+
const relation = normalizeRelation(String(p.relation));
|
|
285
|
+
if (!isKnownRelation(relation, 'global'))
|
|
286
|
+
relViolations++;
|
|
287
|
+
const r = addTriple({
|
|
288
|
+
src: personId,
|
|
289
|
+
relation,
|
|
290
|
+
dst: String(p.dst),
|
|
291
|
+
fact: p.fact ? String(p.fact) : undefined,
|
|
292
|
+
scope: 'global',
|
|
293
|
+
});
|
|
294
|
+
if (r.ok && !r.duplicate)
|
|
295
|
+
profiles++;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
219
298
|
const gc = gcMemories();
|
|
220
|
-
result =
|
|
299
|
+
result.saves = saves;
|
|
300
|
+
result.updates = updates;
|
|
301
|
+
result.forgets = forgets;
|
|
302
|
+
result.triples = triples;
|
|
303
|
+
result.profiles = profiles;
|
|
304
|
+
result.relViolations = relViolations;
|
|
305
|
+
result.gcDecayed = gc.decayed;
|
|
306
|
+
result.gcCapped = gc.capped;
|
|
307
|
+
result.gcGced = gc.gced;
|
|
221
308
|
return result;
|
|
222
309
|
}
|
|
223
310
|
function normalizeType(t) {
|
|
@@ -231,9 +318,13 @@ function normalizeType(t) {
|
|
|
231
318
|
// ── 后台编排:kickoff / drain / 缓存 ─────────────────────────────────────────
|
|
232
319
|
let inflight = null;
|
|
233
320
|
let lastReflectResult = null;
|
|
234
|
-
/** 摘要串(供 repl flush):存N 改N 忘N 图N;有错误附上。 */
|
|
321
|
+
/** 摘要串(供 repl flush):存N 改N 忘N 图N [画像N 违N];有错误附上。 */
|
|
235
322
|
export function formatReflectResult(r) {
|
|
236
323
|
const parts = [`存${r.saves}`, `改${r.updates}`, `忘${r.forgets}`, `图${r.triples}`];
|
|
324
|
+
if (r.profiles)
|
|
325
|
+
parts.push(`画像${r.profiles}`);
|
|
326
|
+
if (r.relViolations)
|
|
327
|
+
parts.push(`违${r.relViolations}`);
|
|
237
328
|
if (r.gcDecayed || r.gcCapped || r.gcGced) {
|
|
238
329
|
parts.push(`遗忘(衰减${r.gcDecayed}/封顶${r.gcCapped}/清除${r.gcGced})`);
|
|
239
330
|
}
|
|
@@ -256,6 +347,10 @@ export function kickoffReflection(transcript) {
|
|
|
256
347
|
const r = await runReflection(transcript);
|
|
257
348
|
lastReflectResult = r;
|
|
258
349
|
appendLog(`[${r.ts}] ${formatReflectResult(r)}`);
|
|
350
|
+
if (r.error === 'parse-failed' && r.rawParseFailure) {
|
|
351
|
+
// 留证:旧版 parse-failed 无声丢弃,无法排查(实测约三成反思命中)。
|
|
352
|
+
appendLog(` ↳ parse-failed 原始返回: ${r.rawParseFailure.replace(/\s+/g, ' ').slice(0, 500)}`);
|
|
353
|
+
}
|
|
259
354
|
}
|
|
260
355
|
catch (e) {
|
|
261
356
|
appendLog(`[${new Date().toISOString()}] 反思异常: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// relation-vocab.ts —— 受控关系词表(轻量,不含实体类型/domain-range)。
|
|
2
|
+
//
|
|
3
|
+
// 这是 memory-2.2 本体方案里"软封闭世界"的最小落地:只约束**关系名**,不引入实体类型
|
|
4
|
+
// 体系或推理。反思抽取 triples/profile 时优先用词表内关系;词表外关系**仍允许写入**
|
|
5
|
+
// (软策略,不阻断长尾),但会被计数为 relViolation 供观测。等真实数据证明需要,再升级
|
|
6
|
+
// 为完整 ontology.ts(实体 type + domain/range + 逆/传递)。
|
|
7
|
+
//
|
|
8
|
+
// 纯常量 + 纯函数,叶子模块,无 IO。
|
|
9
|
+
/** 项目轨(懂仓库)首选关系:高复用、可结构化的连接。 */
|
|
10
|
+
export const PROJECT_RELATIONS = [
|
|
11
|
+
'depends_on',
|
|
12
|
+
'uses',
|
|
13
|
+
'used_by',
|
|
14
|
+
'calls',
|
|
15
|
+
'part_of',
|
|
16
|
+
'implemented_in',
|
|
17
|
+
'reads',
|
|
18
|
+
'writes',
|
|
19
|
+
'configured_by',
|
|
20
|
+
'runs_on',
|
|
21
|
+
'listens_on',
|
|
22
|
+
'tests',
|
|
23
|
+
'documented_in',
|
|
24
|
+
'decided_by',
|
|
25
|
+
'conflicts_with',
|
|
26
|
+
'related_to',
|
|
27
|
+
];
|
|
28
|
+
/** 用户轨(懂用户)画像关系:挂 person-primary。 */
|
|
29
|
+
export const PROFILE_RELATIONS = [
|
|
30
|
+
'named',
|
|
31
|
+
'has_role',
|
|
32
|
+
'works_at',
|
|
33
|
+
'located_in',
|
|
34
|
+
'speaks',
|
|
35
|
+
'verbosity',
|
|
36
|
+
'communication_style',
|
|
37
|
+
'prefers',
|
|
38
|
+
'avoid',
|
|
39
|
+
'avoids',
|
|
40
|
+
'current_goal',
|
|
41
|
+
'constraint',
|
|
42
|
+
'has_hobby',
|
|
43
|
+
'skill_level',
|
|
44
|
+
'related_to',
|
|
45
|
+
];
|
|
46
|
+
/** 常见同义/拼写漂移 → 规范关系(反思落库前归范)。 */
|
|
47
|
+
export const RELATION_ALIASES = {
|
|
48
|
+
employed_by: 'works_at',
|
|
49
|
+
employer: 'works_at',
|
|
50
|
+
lives_in: 'located_in',
|
|
51
|
+
based_in: 'located_in',
|
|
52
|
+
location: 'located_in',
|
|
53
|
+
hobby: 'has_hobby',
|
|
54
|
+
role: 'has_role',
|
|
55
|
+
is_a: 'related_to',
|
|
56
|
+
uses_port: 'listens_on',
|
|
57
|
+
port: 'listens_on',
|
|
58
|
+
depends: 'depends_on',
|
|
59
|
+
dependency: 'depends_on',
|
|
60
|
+
implements: 'implemented_in',
|
|
61
|
+
impl: 'implemented_in',
|
|
62
|
+
decides: 'decided_by',
|
|
63
|
+
conflict: 'conflicts_with',
|
|
64
|
+
configure: 'configured_by',
|
|
65
|
+
};
|
|
66
|
+
const PROJECT_SET = new Set(PROJECT_RELATIONS);
|
|
67
|
+
const PROFILE_SET = new Set(PROFILE_RELATIONS);
|
|
68
|
+
/** 关系名归一:lowercase + 空白转下划线 + 别名映射。 */
|
|
69
|
+
export function normalizeRelation(raw) {
|
|
70
|
+
const base = String(raw ?? '')
|
|
71
|
+
.trim()
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/\s+/g, '_');
|
|
74
|
+
return RELATION_ALIASES[base] ?? base;
|
|
75
|
+
}
|
|
76
|
+
/** 关系是否在对应 scope 的首选词表内(先归一再判定)。 */
|
|
77
|
+
export function isKnownRelation(relation, scope) {
|
|
78
|
+
const r = normalizeRelation(relation);
|
|
79
|
+
return scope === 'global' ? PROFILE_SET.has(r) : PROJECT_SET.has(r);
|
|
80
|
+
}
|
|
81
|
+
/** 注入反思提示词的紧凑词表(按 scope)。 */
|
|
82
|
+
export function relationCheatSheet(scope) {
|
|
83
|
+
return scope === 'global' ? PROFILE_RELATIONS.join(', ') : PROJECT_RELATIONS.join(', ');
|
|
84
|
+
}
|
|
@@ -57,6 +57,9 @@ export function buildPreset(input) {
|
|
|
57
57
|
const contextWindow = typeof model.limit?.context === 'number' && model.limit.context > 0
|
|
58
58
|
? Math.floor(model.limit.context)
|
|
59
59
|
: FALLBACK_CONTEXT_WINDOW;
|
|
60
|
+
// 单次输出上限:目录有 limit.output 就带上(避免吃网关小默认导致长 write_file 被截断);
|
|
61
|
+
// 目录缺失则留 undefined=不下发,绝不硬编码大值(超过模型真实上限会被后端 400 拒整请求)。
|
|
62
|
+
const maxOutput = typeof model.limit?.output === 'number' && model.limit.output > 0 ? Math.floor(model.limit.output) : undefined;
|
|
60
63
|
return {
|
|
61
64
|
name: input.name ?? defaultPresetName(provider.id, model.id),
|
|
62
65
|
provider: presetProvider,
|
|
@@ -64,6 +67,7 @@ export function buildPreset(input) {
|
|
|
64
67
|
apiKey: input.apiKey,
|
|
65
68
|
model: input.modelOverride ?? model.id,
|
|
66
69
|
contextWindow,
|
|
70
|
+
...(maxOutput ? { maxTokens: maxOutput } : {}),
|
|
67
71
|
anthropicPromptCache: presetProvider === 'anthropic',
|
|
68
72
|
};
|
|
69
73
|
}
|
|
@@ -23,6 +23,8 @@ export function applyPresetConfig(ctx, p) {
|
|
|
23
23
|
baseURL: p.baseURL,
|
|
24
24
|
apiKey: p.apiKey,
|
|
25
25
|
contextWindowTokens: p.contextWindow,
|
|
26
|
+
// 预设带值用值;不带显式清除(否则旧 MAX_TOKENS 残留会盖过新预设)。
|
|
27
|
+
maxTokens: p.maxTokens ?? null,
|
|
26
28
|
anthropicPromptCache: p.anthropicPromptCache,
|
|
27
29
|
...(p.reasoningEffort ? { reasoningEffort: p.reasoningEffort } : {}),
|
|
28
30
|
});
|
|
@@ -32,6 +34,7 @@ export function applyPresetConfig(ctx, p) {
|
|
|
32
34
|
LLM_API_KEY: p.apiKey,
|
|
33
35
|
LLM_MODEL: p.model,
|
|
34
36
|
CONTEXT_WINDOW_TOKENS: String(p.contextWindow),
|
|
37
|
+
MAX_TOKENS: p.maxTokens ? String(p.maxTokens) : null,
|
|
35
38
|
ANTHROPIC_PROMPT_CACHE: p.anthropicPromptCache ? 'true' : 'false',
|
|
36
39
|
});
|
|
37
40
|
// 关键不变量:应用预设即激活——必须同步 .active 指针。否则重启时旧激活预设的优先级
|
|
@@ -25,14 +25,15 @@ import { renderHistory } from '../message-format.js';
|
|
|
25
25
|
import { MODEL_PRESETS, maskKey } from '../commands.js';
|
|
26
26
|
import { unhandled, next } from './types.js';
|
|
27
27
|
/** 决定自动存的预设名:协议、缓存配置和连接四元组都一致时不重复存。 */
|
|
28
|
-
function uniquePresetName(desired, provider, baseURL, apiKey, model, contextWindow, anthropicPromptCache) {
|
|
28
|
+
function uniquePresetName(desired, provider, baseURL, apiKey, model, contextWindow, anthropicPromptCache, maxTokens) {
|
|
29
29
|
const existing = listPresets();
|
|
30
30
|
const sameEntry = existing.find((p) => p.provider === provider &&
|
|
31
31
|
p.baseURL === baseURL &&
|
|
32
32
|
p.apiKey === apiKey &&
|
|
33
33
|
p.model === model &&
|
|
34
34
|
p.contextWindow === contextWindow &&
|
|
35
|
-
p.anthropicPromptCache === anthropicPromptCache
|
|
35
|
+
p.anthropicPromptCache === anthropicPromptCache &&
|
|
36
|
+
(p.maxTokens ?? undefined) === maxTokens);
|
|
36
37
|
if (sameEntry)
|
|
37
38
|
return null;
|
|
38
39
|
const sanitized = desired
|
|
@@ -73,6 +74,8 @@ export const modelCommands = [
|
|
|
73
74
|
baseURL: target.baseURL,
|
|
74
75
|
apiKey: target.apiKey,
|
|
75
76
|
contextWindowTokens: target.contextWindow,
|
|
77
|
+
// 预设带值用值;不带则显式清除(否则本窗口/配置文件里的旧 MAX_TOKENS 会一直盖过新预设)。
|
|
78
|
+
maxTokens: target.maxTokens ?? null,
|
|
76
79
|
anthropicPromptCache: target.anthropicPromptCache,
|
|
77
80
|
...(target.reasoningEffort ? { reasoningEffort: target.reasoningEffort } : {}),
|
|
78
81
|
});
|
|
@@ -82,6 +85,8 @@ export const modelCommands = [
|
|
|
82
85
|
LLM_API_KEY: target.apiKey,
|
|
83
86
|
LLM_MODEL: target.model,
|
|
84
87
|
CONTEXT_WINDOW_TOKENS: String(target.contextWindow),
|
|
88
|
+
// null = 从配置文件删除该键(写不下发=吃后端默认)。
|
|
89
|
+
MAX_TOKENS: target.maxTokens ? String(target.maxTokens) : null,
|
|
85
90
|
ANTHROPIC_PROMPT_CACHE: target.anthropicPromptCache ? 'true' : 'false',
|
|
86
91
|
...(target.reasoningEffort ? { REASONING_EFFORT: target.reasoningEffort } : {}),
|
|
87
92
|
});
|
|
@@ -185,6 +190,7 @@ export const modelCommands = [
|
|
|
185
190
|
layout.contentWrite(` ${ui.accent}effort ${ui.reset} ${config.reasoningEffort}` +
|
|
186
191
|
(recognized ? '\n' : `${ui.dim}(当前模型未识别,参数不会下发)${ui.reset}\n`));
|
|
187
192
|
layout.contentWrite(` ${ui.accent}窗口 ${ui.reset} ${config.contextWindowTokens} tokens\n`);
|
|
193
|
+
layout.contentWrite(` ${ui.accent}输出上限${ui.reset} ${config.maxTokens ? `${config.maxTokens} tokens` : `${ui.dim}未设置(不下发,吃后端默认)${ui.reset}`}\n`);
|
|
188
194
|
if (config.provider === 'anthropic') {
|
|
189
195
|
layout.contentWrite(` ${ui.accent}缓存 ${ui.reset} Prompt Cache ${config.anthropicPromptCache ? 'on' : 'off'}\n`);
|
|
190
196
|
}
|
|
@@ -284,11 +290,14 @@ export const modelCommands = [
|
|
|
284
290
|
let apiKey;
|
|
285
291
|
let model;
|
|
286
292
|
let window;
|
|
293
|
+
// 单次输出上限;undefined = 不下发(吃后端默认)。quickApply 沿用当前值。
|
|
294
|
+
let maxTokens;
|
|
287
295
|
if (quickApply) {
|
|
288
296
|
baseURL = preset.baseURL;
|
|
289
297
|
apiKey = config.apiKey;
|
|
290
298
|
model = preset.model;
|
|
291
299
|
window = preset.window;
|
|
300
|
+
maxTokens = config.maxTokens;
|
|
292
301
|
}
|
|
293
302
|
else {
|
|
294
303
|
// baseURL
|
|
@@ -364,6 +373,29 @@ export const modelCommands = [
|
|
|
364
373
|
window = Math.floor(n);
|
|
365
374
|
}
|
|
366
375
|
}
|
|
376
|
+
// maxTokens(可选):单次回复输出上限。回车采纳当前值;填 0/留空 = 不下发吃后端默认。
|
|
377
|
+
{
|
|
378
|
+
const res = await promptIntervention({
|
|
379
|
+
type: 'input',
|
|
380
|
+
title: 'MAX_TOKENS',
|
|
381
|
+
detail: '单次回复最大输出 token(思考+正文+工具参数)。留空=不下发吃后端默认;' +
|
|
382
|
+
'部分兼容网关默认仅 4k/8k,长文件写入会被截断,建议填该模型官方最大输出(常见 8k-64k)。回车采纳当前值。',
|
|
383
|
+
seed: config.maxTokens ? String(config.maxTokens) : '',
|
|
384
|
+
});
|
|
385
|
+
if (res.action === 'cancelled') {
|
|
386
|
+
return next();
|
|
387
|
+
}
|
|
388
|
+
const v = (res.value ?? '').trim();
|
|
389
|
+
if (v) {
|
|
390
|
+
const n = Number(v);
|
|
391
|
+
if (Number.isFinite(n) && n > 0)
|
|
392
|
+
maxTokens = Math.floor(n);
|
|
393
|
+
// 非法值不阻断:保持 undefined(不下发),与留空同义。
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
maxTokens = undefined;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
367
399
|
}
|
|
368
400
|
// 3) 应用协议、连接与缓存配置;Anthropic 原生协议无需重建 OpenAI client,但统一刷新无害。
|
|
369
401
|
const provider = preset.provider;
|
|
@@ -374,6 +406,8 @@ export const modelCommands = [
|
|
|
374
406
|
baseURL,
|
|
375
407
|
apiKey,
|
|
376
408
|
contextWindowTokens: window,
|
|
409
|
+
// 手填向导里留空 = undefined → 显式清除旧值,不下发 max_tokens。
|
|
410
|
+
maxTokens: maxTokens ?? null,
|
|
377
411
|
anthropicPromptCache,
|
|
378
412
|
});
|
|
379
413
|
writeConfigKeys({
|
|
@@ -382,13 +416,14 @@ export const modelCommands = [
|
|
|
382
416
|
LLM_API_KEY: apiKey,
|
|
383
417
|
LLM_MODEL: model,
|
|
384
418
|
CONTEXT_WINDOW_TOKENS: String(window),
|
|
419
|
+
MAX_TOKENS: maxTokens ? String(maxTokens) : null,
|
|
385
420
|
ANTHROPIC_PROMPT_CACHE: anthropicPromptCache ? 'true' : 'false',
|
|
386
421
|
});
|
|
387
422
|
reconfigureClient();
|
|
388
423
|
// 3.5) 自动存为命名预设;协议与缓存策略也是去重键的一部分。
|
|
389
424
|
let savedName = null;
|
|
390
425
|
try {
|
|
391
|
-
const finalName = uniquePresetName(model, provider, baseURL, apiKey, model, window, anthropicPromptCache);
|
|
426
|
+
const finalName = uniquePresetName(model, provider, baseURL, apiKey, model, window, anthropicPromptCache, maxTokens);
|
|
392
427
|
if (finalName) {
|
|
393
428
|
savePreset({
|
|
394
429
|
name: finalName,
|
|
@@ -397,6 +432,7 @@ export const modelCommands = [
|
|
|
397
432
|
apiKey,
|
|
398
433
|
model,
|
|
399
434
|
contextWindow: window,
|
|
435
|
+
...(maxTokens ? { maxTokens } : {}),
|
|
400
436
|
anthropicPromptCache,
|
|
401
437
|
});
|
|
402
438
|
savedName = finalName;
|
package/dist/repl/runtime.js
CHANGED
|
@@ -24,7 +24,7 @@ import { renderChip } from '../attachments/image.js';
|
|
|
24
24
|
import { contextState, appendCurrentSessionRuntimeEvent } from '../session/index.js';
|
|
25
25
|
import { effectiveSystemPrompt } from '../skills/index.js';
|
|
26
26
|
import { clearSkillActivation } from '../skills/activation.js';
|
|
27
|
-
import { buildMemoryIndexSection, kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, } from '../memory/index.js';
|
|
27
|
+
import { buildMemoryIndexSection, buildAboutYouSection, buildProjectGraphHint, initPersona, kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, } from '../memory/index.js';
|
|
28
28
|
import { collectQueryHistory } from '../session/query-history.js';
|
|
29
29
|
import { ToolPolicyController } from '../tools/policy.js';
|
|
30
30
|
import { routeToolGroups } from '../tools/router.js';
|
|
@@ -198,9 +198,18 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
198
198
|
// 与开关联动:① base 用 buildBasePrompt() 取代 config.systemPrompt(后者是启动时一次性
|
|
199
199
|
// 求值的常量,运行时 /memory_switch 不会刷新);② plan suffix 走 getPlanModeSuffix() 现拼;
|
|
200
200
|
// ③ AGENTS.md 存在工作区根时由 base 无条件自动导入正文(超长截断,与 memory 开关无关);④ Memory Index 按开关注入。
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
// 启动期一次性身份归一(幂等,best-effort):把全局图里的用户指称碎片并入
|
|
202
|
+
// person-primary,随后 About you 段才能渲染出完整画像。
|
|
203
|
+
initPersona(isMemoryEnabled());
|
|
204
|
+
// 开关现读:运行时 /memory_switch 重建 history[0] 时,About you 与 Memory Index 同步显隐。
|
|
205
|
+
const buildSystemMessage = (planMode) => {
|
|
206
|
+
const memoryOn = isMemoryEnabled();
|
|
207
|
+
return effectiveSystemPrompt(buildBasePrompt(currentSessionId) +
|
|
208
|
+
buildAboutYouSection(memoryOn) +
|
|
209
|
+
(planMode ? getPlanModeSuffix() : '') +
|
|
210
|
+
buildMemoryIndexSection(memoryOn) +
|
|
211
|
+
buildProjectGraphHint(memoryOn));
|
|
212
|
+
};
|
|
204
213
|
// 有预加载(--resume)则用它,并把 history[0] 刷成当前 system prompt(config 可能已变);
|
|
205
214
|
// 否则新会话只塞 system 提示(默认 auto)。
|
|
206
215
|
const history = initialHistory && initialHistory.length ? initialHistory : [{ role: 'system', content: buildSystemMessage(false) }];
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -274,7 +274,7 @@ export class Runtime {
|
|
|
274
274
|
}
|
|
275
275
|
resumeSession(id) {
|
|
276
276
|
this.assertIdle('resume session');
|
|
277
|
-
const loaded = this.context.sessionStore.load(id);
|
|
277
|
+
const loaded = this.context.sessionStore.load(id, { restoreAssets: true });
|
|
278
278
|
if (!loaded?.history.length)
|
|
279
279
|
return null;
|
|
280
280
|
this.context.sessionStore.setCurrentSessionId(loaded.id);
|