mocode-ai 1.3.9 → 1.4.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/README.md +78 -70
- package/README.zh-CN.md +81 -73
- package/bin/mocode-agent-host.js +1 -1
- package/dist/agent/core.js +226 -293
- package/dist/agent/index.js +14 -10
- package/dist/agent/mode.js +3 -3
- package/dist/agent/runtime-context.js +45 -0
- package/dist/agent/spawn.js +101 -86
- package/dist/agent/tool-helpers.js +134 -0
- package/dist/agent/trace-state.js +118 -0
- package/dist/agents/coordinator.js +6 -1
- package/dist/changeset/index.js +9 -8
- package/dist/config/index.js +109 -77
- package/dist/config/presets.js +3 -1
- package/dist/config/profiles.js +125 -0
- package/dist/context/age-aware.js +2 -4
- package/dist/context/artifacts.js +6 -13
- package/dist/context/budget.js +6 -8
- package/dist/context/classifier.js +1 -2
- package/dist/context/encoders/code.js +2 -6
- package/dist/context/encoders/command.js +1 -3
- package/dist/context/encoders/search.js +1 -3
- package/dist/context/index.js +1 -1
- package/dist/context/lifecycle.js +5 -3
- package/dist/context/pipeline.js +1 -1
- package/dist/context/relevance.js +2 -4
- package/dist/context/token-calibration.js +13 -18
- package/dist/host/stdio.js +73 -12
- package/dist/i18n/index.js +96 -44
- package/dist/index.js +1 -1
- package/dist/llm/index.js +39 -23
- package/dist/llm/provider.js +23 -0
- package/dist/llm/providers/anthropic.js +12 -18
- package/dist/llm/tool-schema.js +48 -0
- package/dist/mcp/client.js +70 -25
- package/dist/mcp/config.js +1 -3
- package/dist/mcp/index.js +3 -1
- package/dist/memory/reflect.js +3 -6
- package/dist/memory/store.js +10 -12
- package/dist/permissions/index.js +66 -22
- package/dist/pet/bridge.js +2 -2
- package/dist/repl/commands/appearance.js +72 -0
- package/dist/repl/commands/compact.js +114 -0
- package/dist/repl/commands/context.js +17 -0
- package/dist/repl/commands/image.js +71 -0
- package/dist/repl/commands/memory.js +51 -0
- package/dist/repl/commands/mode.js +47 -0
- package/dist/repl/commands/model.js +435 -0
- package/dist/repl/commands/pet.js +70 -0
- package/dist/repl/commands/registry.js +46 -0
- package/dist/repl/commands/session.js +102 -0
- package/dist/repl/commands/skill.js +95 -0
- package/dist/repl/commands/system.js +202 -0
- package/dist/repl/commands/tool-group.js +233 -0
- package/dist/repl/commands/types.js +4 -0
- package/dist/repl/commands.js +324 -0
- package/dist/repl/index.js +5 -2577
- package/dist/repl/message-format.js +151 -0
- package/dist/repl/running-input.js +147 -0
- package/dist/repl/runtime.js +816 -0
- package/dist/repl/status-bar.js +161 -0
- package/dist/rollback/index.js +44 -14
- package/dist/runtime/browser-manager.js +1 -1
- package/dist/runtime/dev-server-manager.js +1 -1
- package/dist/runtime/input-injector.js +313 -0
- package/dist/runtime/screen-capture.js +89 -0
- package/dist/runtime/screen-pipeline.js +223 -0
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +8 -2
- package/dist/session/compact.js +22 -34
- package/dist/session/index.js +2 -2
- package/dist/session/notes.js +39 -5
- package/dist/session/persist.js +14 -8
- package/dist/session/scheduler.js +3 -4
- package/dist/session/trace-metrics.js +6 -3
- package/dist/skills/activation.js +10 -5
- package/dist/skills/discover.js +5 -17
- package/dist/skills/index.js +1 -1
- package/dist/skills/runner.js +28 -44
- package/dist/skills/toolmap.js +5 -2
- package/dist/tools/builtins/browser.js +17 -3
- package/dist/tools/builtins/computer.js +322 -0
- package/dist/tools/builtins/dev-server.js +1 -3
- package/dist/tools/builtins/edit-file.js +21 -7
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +28 -25
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-graph.js +4 -7
- package/dist/tools/builtins/memory-list.js +2 -6
- package/dist/tools/builtins/memory-save.js +8 -2
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/note-append.js +14 -3
- package/dist/tools/builtins/plan-update.js +40 -9
- package/dist/tools/builtins/run-command.js +8 -8
- package/dist/tools/builtins/screenshot.js +11 -87
- package/dist/tools/builtins/task.js +29 -40
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/view-image.js +5 -3
- package/dist/tools/builtins/web-fetch.js +1 -3
- package/dist/tools/builtins/web-search.js +3 -7
- package/dist/tools/builtins/write-file.js +4 -2
- package/dist/tools/constants.js +50 -37
- package/dist/tools/policy.js +221 -0
- package/dist/tools/registry.js +40 -19
- package/dist/tools/resource-lock.js +4 -2
- package/dist/tools/router.js +143 -0
- package/dist/tools/validation.js +6 -5
- package/dist/ui/batch.js +63 -23
- package/dist/ui/clipboard.js +17 -5
- package/dist/ui/content.js +166 -13
- package/dist/ui/diff.js +61 -19
- package/dist/ui/fuzzy.js +35 -2
- package/dist/ui/intervention.js +2 -4
- package/dist/ui/layout-internal/content-write.js +587 -0
- package/dist/ui/layout-internal/core.js +590 -0
- package/dist/ui/layout-internal/input-paint.js +365 -0
- package/dist/ui/layout-internal/screen.js +128 -0
- package/dist/ui/layout-internal/scroll.js +152 -0
- package/dist/ui/layout-internal/selection.js +229 -0
- package/dist/ui/layout-internal/state.js +82 -0
- package/dist/ui/layout-internal/statusbar.js +351 -0
- package/dist/ui/layout-types.js +1 -0
- package/dist/ui/layout.js +3 -2288
- package/dist/ui/markdown.js +10 -2
- package/dist/ui/prompt-internal/editor.js +945 -0
- package/dist/ui/prompt-internal/paste.js +52 -0
- package/dist/ui/prompt-internal/pickers.js +519 -0
- package/dist/ui/prompt-internal/types.js +1 -0
- package/dist/ui/prompt.js +4 -1506
- package/dist/ui/render.js +87 -2
- package/dist/verification/affected.js +11 -5
- package/dist/verification/discovery.js +1 -3
- package/dist/verification/profile.js +11 -3
- package/package.json +11 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent 单轮的 trace / token-usage 状态聚合(从 core.ts 提取,2.0 步骤2 深拆第一刀)。
|
|
3
|
+
*
|
|
4
|
+
* runAgentCore 原本把 emitTrace / addUsage / reportLive 写成函数内闭包,捕获
|
|
5
|
+
* traceSessionId / traceTurnId / currentTraceStep / abortTraced / turnUsage 等一坨可变局部量。
|
|
6
|
+
* 这里把这坨状态收敛进一个对象,runAgentCore 持有实例并调用方法,行为与字节级事件 payload 不变。
|
|
7
|
+
*
|
|
8
|
+
* 设计约束:
|
|
9
|
+
* - emit() 的 currentTraceStep 三元逻辑逐字保留——trace 事件落盘格式是观测契约,不能漂移。
|
|
10
|
+
* - addUsage() 的逐字段累加(含 cacheCreationTokens 的 ?? 0 兜底)逐字保留。
|
|
11
|
+
* - reportLive() 的 onLiveUsage 上报警文结构与 turnUsage 闭包累加口径一致(实时 chip 显示)。
|
|
12
|
+
* - turnId 类型为 number | undefined:traceTurnId 兜底链可能产出 undefined(--no-session)。
|
|
13
|
+
*/
|
|
14
|
+
import { createTraceEvent } from '../session/index.js';
|
|
15
|
+
export class TurnTraceState {
|
|
16
|
+
sessionId;
|
|
17
|
+
turnId;
|
|
18
|
+
/** 当前步序号;循环每步开头赋值,finally 归 undefined。stepId 由此拼。 */
|
|
19
|
+
currentTraceStep;
|
|
20
|
+
/** abort 事件只记一次的幂等旗标。 */
|
|
21
|
+
abortTraced = false;
|
|
22
|
+
/** 本轮工具调用总数(turn_end 摘要用)。 */
|
|
23
|
+
toolCallCount = 0;
|
|
24
|
+
/** 本轮 token 累计;未开启 include_usage 或全失败时为 undefined。 */
|
|
25
|
+
turnUsage;
|
|
26
|
+
onTraceEvent;
|
|
27
|
+
constructor(init) {
|
|
28
|
+
this.sessionId = init.sessionId;
|
|
29
|
+
this.turnId = init.turnId;
|
|
30
|
+
this.onTraceEvent = init.onTraceEvent;
|
|
31
|
+
}
|
|
32
|
+
/** 记一条 trace 事件。best-effort:回调抛错被吞,绝不改变执行路径。 */
|
|
33
|
+
emit(type, data = {}, ids = {}) {
|
|
34
|
+
const turnId = this.turnId;
|
|
35
|
+
if (turnId === undefined)
|
|
36
|
+
return; // 无会话上下文(--no-session)不产生事件,与原闭包行为一致
|
|
37
|
+
try {
|
|
38
|
+
this.onTraceEvent?.(createTraceEvent({
|
|
39
|
+
sessionId: this.sessionId,
|
|
40
|
+
turnId,
|
|
41
|
+
type,
|
|
42
|
+
...(this.currentTraceStep === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: {
|
|
45
|
+
step: this.currentTraceStep,
|
|
46
|
+
stepId: `${turnId}:step:${this.currentTraceStep}`,
|
|
47
|
+
}),
|
|
48
|
+
...ids,
|
|
49
|
+
data,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Trace is best-effort and must never alter execution.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** 累加一次 chat 返回的真实 usage 到本轮 turnUsage。 */
|
|
57
|
+
addUsage(u) {
|
|
58
|
+
if (!u)
|
|
59
|
+
return;
|
|
60
|
+
this.turnUsage = this.turnUsage
|
|
61
|
+
? {
|
|
62
|
+
promptTokens: this.turnUsage.promptTokens + u.promptTokens,
|
|
63
|
+
completionTokens: this.turnUsage.completionTokens + u.completionTokens,
|
|
64
|
+
totalTokens: this.turnUsage.totalTokens + u.totalTokens,
|
|
65
|
+
cachedTokens: this.turnUsage.cachedTokens + u.cachedTokens,
|
|
66
|
+
cacheCreationTokens: (this.turnUsage.cacheCreationTokens ?? 0) + (u.cacheCreationTokens ?? 0),
|
|
67
|
+
reasoningTokens: this.turnUsage.reasoningTokens + u.reasoningTokens,
|
|
68
|
+
}
|
|
69
|
+
: u;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 实时用量上报:onLiveUsage 推送「已完成步实测(turnUsage) + 当前步(prompt 估算/实测 + 流式 completion)」。
|
|
73
|
+
* turnUsage 在本对象内被 addUsage 原地累加,这里每次读最新值,口径与轮末摘要一致。
|
|
74
|
+
*/
|
|
75
|
+
reportLive(hooks, stepPromptEst, lastStepPromptTokens, providerCacheSeen, p) {
|
|
76
|
+
// 当前步 prompt:末尾 usage chunk 到达后用实测,流式期间用估算(含校准)。
|
|
77
|
+
// 当前步 cache 命中同理:上报即用实测;流式期间按前缀缓存估算 ≈ 上一步实测 prompt
|
|
78
|
+
// (当前 prompt 总含其为前缀),不超过当前步 prompt;后端从不报 cache 时不估算。
|
|
79
|
+
// 口径与轮末摘要一致:chip ↑ 显计费 prompt(裸 - cached),↓/↻ 同。
|
|
80
|
+
const curPrompt = p.promptTokens ?? stepPromptEst;
|
|
81
|
+
const curCached = p.cachedTokens ?? (providerCacheSeen ? Math.min(lastStepPromptTokens, curPrompt) : 0);
|
|
82
|
+
hooks.onLiveUsage?.({
|
|
83
|
+
promptTokens: (this.turnUsage?.promptTokens ?? 0) + curPrompt,
|
|
84
|
+
completionTokens: (this.turnUsage?.completionTokens ?? 0) + p.completionTokens,
|
|
85
|
+
totalTokens: (this.turnUsage?.totalTokens ?? 0) + curPrompt + p.completionTokens,
|
|
86
|
+
cachedTokens: (this.turnUsage?.cachedTokens ?? 0) + curCached,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 中断还原:记一次 abort 事件(幂等)→ onAbort 钩子 → history 回滚到快照 → 模式还原。
|
|
91
|
+
* 与 core.ts 原 abortRestore 闭包逐字一致(含调用顺序);traceStatus 赋值留在 core
|
|
92
|
+
* (它是 core 局部 let,turn_end 埋点还要读)。
|
|
93
|
+
*/
|
|
94
|
+
abortRestore(deps) {
|
|
95
|
+
if (!this.abortTraced) {
|
|
96
|
+
this.emit('abort', { phase: 'observed', reason: 'signal' });
|
|
97
|
+
this.abortTraced = true;
|
|
98
|
+
}
|
|
99
|
+
deps.hooks.onAbort?.();
|
|
100
|
+
deps.history.length = 0;
|
|
101
|
+
deps.history.push(...deps.savedHistory);
|
|
102
|
+
deps.ctx.setAgentMode(deps.savedMode);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 构造中断返回值(completed=false / terminationReason='aborted' / finalText=null)。
|
|
106
|
+
* core.ts 两处 aborted-return 块逐字节相同,收敛到此处;mutation 由调用方现取
|
|
107
|
+
* (ctx.getCurrentTurnMutationState()),保证读取时点与原内联代码一致。
|
|
108
|
+
*/
|
|
109
|
+
buildAbortedResult(mutation) {
|
|
110
|
+
return {
|
|
111
|
+
completed: false,
|
|
112
|
+
terminationReason: 'aborted',
|
|
113
|
+
finalText: null,
|
|
114
|
+
usage: this.turnUsage,
|
|
115
|
+
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -33,7 +33,12 @@ function diffToChangeSet(before, after) {
|
|
|
33
33
|
else if (oldValue && !newValue)
|
|
34
34
|
changes.push({ path: file, operation: 'delete', expectedHash: contentHash(oldValue) });
|
|
35
35
|
else if (oldValue && newValue)
|
|
36
|
-
changes.push({
|
|
36
|
+
changes.push({
|
|
37
|
+
path: file,
|
|
38
|
+
operation: 'update',
|
|
39
|
+
expectedHash: contentHash(oldValue),
|
|
40
|
+
replacement: text(newValue),
|
|
41
|
+
});
|
|
37
42
|
}
|
|
38
43
|
return changes.length ? createChangeSet(changes) : null;
|
|
39
44
|
}
|
package/dist/changeset/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { chmod, copyFile, mkdir, readFile, rename, rm, rmdir, stat, writeFile
|
|
2
|
+
import { chmod, copyFile, mkdir, readFile, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { jailResolve } from '../sandbox/index.js';
|
|
6
6
|
import { beginPathMutation, endPathMutation } from '../rollback/index.js';
|
|
7
|
-
import { canonicalFileResourceKey, toolResourceLockManager
|
|
7
|
+
import { canonicalFileResourceKey, toolResourceLockManager } from '../tools/resource-lock.js';
|
|
8
8
|
export function contentHash(content) {
|
|
9
9
|
const data = typeof content === 'string' ? Buffer.from(content, 'utf8') : content;
|
|
10
10
|
return `sha256:${createHash('sha256').update(data).digest('hex')}`;
|
|
@@ -19,8 +19,11 @@ function applyTextEdits(source, edits) {
|
|
|
19
19
|
let cursor = 0;
|
|
20
20
|
let output = '';
|
|
21
21
|
for (const edit of ordered) {
|
|
22
|
-
if (!Number.isInteger(edit.start) ||
|
|
23
|
-
|
|
22
|
+
if (!Number.isInteger(edit.start) ||
|
|
23
|
+
!Number.isInteger(edit.end) ||
|
|
24
|
+
edit.start < cursor ||
|
|
25
|
+
edit.end < edit.start ||
|
|
26
|
+
edit.end > source.length) {
|
|
24
27
|
throw new Error(`无效或重叠的 TextEdit 范围: ${edit.start}..${edit.end}`);
|
|
25
28
|
}
|
|
26
29
|
output += source.slice(cursor, edit.start) + edit.newText;
|
|
@@ -111,9 +114,7 @@ export async function dryRunChangeSet(changeSet) {
|
|
|
111
114
|
});
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
|
-
return conflicts.length > 0
|
|
115
|
-
? { ok: false, conflicts }
|
|
116
|
-
: { ok: true, changeSet: { ...changeSet, prepared } };
|
|
117
|
+
return conflicts.length > 0 ? { ok: false, conflicts } : { ok: true, changeSet: { ...changeSet, prepared } };
|
|
117
118
|
}
|
|
118
119
|
async function removeIfPresent(target) {
|
|
119
120
|
await rm(target, { recursive: true, force: true });
|
|
@@ -222,7 +223,7 @@ export async function commitChangeSet(changeSet, signal) {
|
|
|
222
223
|
if (temp)
|
|
223
224
|
await rename(temp, change.absolutePath);
|
|
224
225
|
}
|
|
225
|
-
for (const {
|
|
226
|
+
for (const { capture } of captures)
|
|
226
227
|
endPathMutation(capture, `changeset:${changeSet.id}`);
|
|
227
228
|
for (const backup of backupByPath.values()) {
|
|
228
229
|
await removeIfPresent(backup).catch(() => undefined);
|
package/dist/config/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import { getNotesFilePath, extractActiveNotesSections } from '../session/notes.j
|
|
|
7
7
|
import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
|
|
8
8
|
import { buildValidationCommandsSection } from '../verification/prompt.js';
|
|
9
9
|
import { getActivePresetName, readPreset } from './presets.js';
|
|
10
|
-
import { detectLanguage, setLanguage, t
|
|
10
|
+
import { detectLanguage, setLanguage, t } from '../i18n/index.js';
|
|
11
|
+
import { isProfileName, profileHasGroup } from './profiles.js';
|
|
11
12
|
/**
|
|
12
13
|
* 按优先级加载配置文件并回填 process.env:
|
|
13
14
|
* 候选(后者覆盖前者,优先级升序):<cwd>/.env(兼容旧用法,最低)→ ~/.mocode/config(全局)→ <cwd>/.mocode/config(项目级覆盖,最高)。
|
|
@@ -42,10 +43,25 @@ export const languageFromShell = process.env.MOCODE_LANGUAGE !== undefined;
|
|
|
42
43
|
// 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
|
|
43
44
|
// 仿 themeFromShell 模式:shell export 的环境变量在 loadEnvFiles 中不被回填(优先级最高),
|
|
44
45
|
// 故 /model 写入 ~/.mocode/config 的同名键下次启动会被 shell 值覆盖——据此给 dim 警告。
|
|
45
|
-
const LLM_ENV_KEYS = [
|
|
46
|
+
const LLM_ENV_KEYS = [
|
|
47
|
+
'LLM_PROVIDER',
|
|
48
|
+
'LLM_BASE_URL',
|
|
49
|
+
'LLM_API_KEY',
|
|
50
|
+
'LLM_MODEL',
|
|
51
|
+
'CONTEXT_WINDOW_TOKENS',
|
|
52
|
+
'ANTHROPIC_PROMPT_CACHE',
|
|
53
|
+
];
|
|
46
54
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 256000;
|
|
47
55
|
const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
|
|
48
56
|
loadEnvFiles();
|
|
57
|
+
/**
|
|
58
|
+
* 旧 profile 兼容状态:仅供未传 ToolPolicy 的嵌入调用与迁移测试。官方 TUI/stdio
|
|
59
|
+
* 每个真实用户 turn 都使用 LLM route,且不再提供 /profile 用户命令。
|
|
60
|
+
*/
|
|
61
|
+
let activeProfile = isProfileName(process.env.MOCODE_MODE) ? process.env.MOCODE_MODE : 'coding';
|
|
62
|
+
if (process.env.MOCODE_MODE !== undefined && !isProfileName(process.env.MOCODE_MODE)) {
|
|
63
|
+
console.warn(`[mocode] 未知 MOCODE_MODE="${process.env.MOCODE_MODE}",回退 coding(可选: coding|frontend|computer-use|research|full)`);
|
|
64
|
+
}
|
|
49
65
|
/**
|
|
50
66
|
* 激活预设覆盖:若用户曾用 /model 激活过预设,启动时让上下文窗口等配置**跟随该预设文件**,
|
|
51
67
|
* 而不是只信 config 文件里上一组裸 LLM 键——这正是"切换后窗口不再退回 256k"的关键。
|
|
@@ -123,11 +139,6 @@ function readPersonaFile() {
|
|
|
123
139
|
function buildVoiceSection() {
|
|
124
140
|
return readPersonaFile() || DEFAULT_VOICE;
|
|
125
141
|
}
|
|
126
|
-
/**
|
|
127
|
-
* 基础系统提示的"记忆段落":开 isMemoryEnabled() 时才拼。
|
|
128
|
-
* 默认关(新用户零侵入):这段 + 工具表里的 5 个 memory_* + 系统提示尾部的 Memory Index
|
|
129
|
-
* 都不出现;打开 /memory_switch 后下一次新建 system message 才注入。
|
|
130
|
-
*/
|
|
131
142
|
/**
|
|
132
143
|
* Session notepad 段落:读取 .mocode/sessions/<sessionId>/notes.md,只注入 ## 标题行作为目录摘要。
|
|
133
144
|
* Agent 用 write_file/edit_file/read_file 维护此文件,抗 compact(在 context window 之外)。
|
|
@@ -147,8 +158,9 @@ export function buildNotepadSection(sessionId = getCurrentSessionId()) {
|
|
|
147
158
|
if (!content)
|
|
148
159
|
return '';
|
|
149
160
|
// 1) 提取 ## 标题行(最多 15 个,按文件出现顺序保留)
|
|
150
|
-
const headers = content
|
|
151
|
-
.
|
|
161
|
+
const headers = content
|
|
162
|
+
.split('\n')
|
|
163
|
+
.filter((l) => /^##\s/.test(l))
|
|
152
164
|
.slice(0, 15);
|
|
153
165
|
// 2) 分桶:Done: 开头 → archived;其余 → active
|
|
154
166
|
// "## Plan:" 和 "## Open Questions" 视为永久 active(不需要改名为 Done)。
|
|
@@ -167,11 +179,11 @@ export function buildNotepadSection(sessionId = getCurrentSessionId()) {
|
|
|
167
179
|
'',
|
|
168
180
|
`## Session Notepad index (${totalCount} section${totalCount === 1 ? '' : 's'} — read \`.mocode/sessions/${sessionId}/notes.md\` to recover full context; surviving compact is the whole point of this file)`,
|
|
169
181
|
`Active (${active.length}):`,
|
|
170
|
-
...(active.length ? active.map(h => ` - ${h.replace(/^##\s+/, '')}`) : [' - (none)']),
|
|
182
|
+
...(active.length ? active.map((h) => ` - ${h.replace(/^##\s+/, '')}`) : [' - (none)']),
|
|
171
183
|
];
|
|
172
184
|
if (archived.length) {
|
|
173
185
|
lines.push(`Done (${archived.length}):`);
|
|
174
|
-
lines.push(...archived.map(h => ` - ${h.replace(/^##\s+/, '')}`));
|
|
186
|
+
lines.push(...archived.map((h) => ` - ${h.replace(/^##\s+/, '')}`));
|
|
175
187
|
}
|
|
176
188
|
lines.push('');
|
|
177
189
|
return lines.join('\n');
|
|
@@ -295,11 +307,6 @@ export function buildSessionStateReminder(sessionId = getCurrentSessionId()) {
|
|
|
295
307
|
];
|
|
296
308
|
return parts.join('\n\n');
|
|
297
309
|
}
|
|
298
|
-
const SYSTEM_PROMPT_MEMORY_SECTION = `
|
|
299
|
-
## Memory (cross-session facts)
|
|
300
|
-
- 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.
|
|
301
|
-
- Save only stable, non-obvious cross-session facts. Search before saving; update an existing entry instead of duplicating it, and archive stale entries.
|
|
302
|
-
- A knowledge-graph layer links entities across memories: explore relations/neighbors with memory_graph (neighbors/add/stats), and attach meaningful links via the links parameter of memory_save when saving.`;
|
|
303
310
|
/** AGENTS.md 自动导入正文上限:system 位于 history[0] 且 compactHistory 不压缩 system,超长需截断防占窗口(见 memory/README.md)。 */
|
|
304
311
|
const MAX_AGENTS_IMPORT_CHARS = 20000;
|
|
305
312
|
/**
|
|
@@ -324,30 +331,16 @@ function buildAgentsImportSection() {
|
|
|
324
331
|
return ''; // 读失败静默跳过:不让导入破坏 prompt 构建
|
|
325
332
|
}
|
|
326
333
|
}
|
|
327
|
-
/**
|
|
328
|
-
* Memory 检索指导段(与 AGENTS.md 导入无关):开 isMemoryEnabled() 时才拼,
|
|
329
|
-
* 注入 memory_search/list/graph 的使用指导。默认关(新用户零侵入)。
|
|
330
|
-
*/
|
|
331
|
-
function buildMemoryPromptSection() {
|
|
332
|
-
if (!isMemoryEnabled())
|
|
333
|
-
return '';
|
|
334
|
-
return SYSTEM_PROMPT_MEMORY_SECTION;
|
|
335
|
-
}
|
|
336
334
|
/**
|
|
337
335
|
* plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
|
|
338
336
|
* 与 SYSTEM_PROMPT 同语种(英文),指示:只读探查、产出步骤化计划、不执行、审批后回 auto。
|
|
339
337
|
*
|
|
340
|
-
*
|
|
341
|
-
* 句都不出现,且 read-only 列表里的 memory_search/memory_list 也移除——避免提示词里出现
|
|
342
|
-
* 根本不存在的工具名引起 LLM 调不到。
|
|
338
|
+
* 工具名不在这里静态枚举;plan snapshot 会从当前自动路由结果中剔除所有写入/执行能力。
|
|
343
339
|
*
|
|
344
|
-
* .codegraph/
|
|
345
|
-
* 没索引时干脆不提,避免 LLM 调出失败。函数化(非 const)以便在 buildPlanModeSuffix 里现拼。
|
|
340
|
+
* .codegraph/ 存在性动态决定是否提示 codegraph skill。
|
|
346
341
|
*/
|
|
347
342
|
function buildPlanResearchRules() {
|
|
348
|
-
const cg = hasCodegraphIndex()
|
|
349
|
-
? ' Prefer the available codegraph skill for call paths and blast radius.'
|
|
350
|
-
: '';
|
|
343
|
+
const cg = hasCodegraphIndex() ? ' Prefer the available codegraph skill for call paths and blast radius.' : '';
|
|
351
344
|
return `
|
|
352
345
|
- Locate relevant code and conventions without repeating retrieved work.${cg}
|
|
353
346
|
- Return an actionable plan with affected files, ordered steps, edge cases, and verification.
|
|
@@ -363,7 +356,6 @@ ${buildPlanResearchRules()}`;
|
|
|
363
356
|
/** 兼容旧名字:repl 的 buildSystemMessage 仍引 PLAN_MODE_SUFFIX(变量)。运行时按需现拼。 */
|
|
364
357
|
export function buildBasePrompt(sessionId = getCurrentSessionId()) {
|
|
365
358
|
const agentsImportSection = buildAgentsImportSection();
|
|
366
|
-
const memorySection = buildMemoryPromptSection();
|
|
367
359
|
const notepadSection = buildNotepadSection(sessionId);
|
|
368
360
|
// 静态主体:稳定段落集中在前,让支持 prompt caching 的后端能命中前缀缓存(#12)。
|
|
369
361
|
// 约束:staticBody 的前缀段(尤其 ## Identity 第一行)必须是纯静态文本,
|
|
@@ -382,7 +374,7 @@ Complete programming tasks through an "analyze → call tool → observe result
|
|
|
382
374
|
|
|
383
375
|
## Workflow
|
|
384
376
|
- Understand: use existing conversation and tool evidence before gathering more; inspect only what supports the next decision, do not guess.
|
|
385
|
-
- Plan: for tasks with 3+ steps or context-loss risk, record the plan with the \`plan_update\` tool (see Session state);
|
|
377
|
+
- Plan: for tasks with 3+ steps or context-loss risk, record the plan with the \`plan_update\` tool (see Session state); give each step a short title plus a self-contained content.
|
|
386
378
|
- Implement: make the smallest coherent change; edit against a fresh read (see Tool policy); avoid unrelated refactors.
|
|
387
379
|
- Verify: decide whether validation is useful by risk and scope; run the smallest relevant check, not broad test/build suites by default.
|
|
388
380
|
- Report: stop when done and give honest conclusions with path:line references (see Reporting).
|
|
@@ -419,10 +411,9 @@ ${buildVoiceSection()}
|
|
|
419
411
|
- **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
|
|
420
412
|
- Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.
|
|
421
413
|
${t('assistant.languageInstruction')}`;
|
|
422
|
-
// 动态段(置于末尾):AGENTS.md 项目记忆
|
|
414
|
+
// 动态段(置于末尾):AGENTS.md 项目记忆 + notepad 索引/说明。工具簇特定指导由
|
|
415
|
+
// ToolPolicyController.reminder() 按当前 turn 的 route 注入,避免旧全局 profile 与真实 schema 分裂。
|
|
423
416
|
// 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
|
|
424
|
-
// - "## Project context" 仅当 agentsImportSection/memorySection/notepadSection 任一非空(notepad 索引依赖 notes.md 存在);
|
|
425
|
-
// - "## Session state" 使用说明**无条件**注入(放在动态尾段首位):否则会陷入"说明依赖 notes.md 存在 → 模型不知要建 → 文件永不存在"的鸡生蛋循环,功能对模型不可见。动态段在静态前缀之后,不影响 prompt 缓存。
|
|
426
417
|
const dynamicParts = [];
|
|
427
418
|
// 会话级私有尾段(子 agent 切片会丢弃):Session state 说明无条件注入在前,Project context 按需在后。
|
|
428
419
|
dynamicParts.push(`## Session state (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
|
|
@@ -432,17 +423,18 @@ ${t('assistant.languageInstruction')}`;
|
|
|
432
423
|
'## Plan: <title>\n' +
|
|
433
424
|
'Goal: <outcome>\n' +
|
|
434
425
|
'### Steps\n' +
|
|
435
|
-
'- [ ] 1. <self-contained step: target file/symbol, the change, and how to verify>\n' +
|
|
426
|
+
'- [ ] 1. **<short label, ≤20 chars>** — <self-contained step: target file/symbol, the change, and how to verify>\n' +
|
|
436
427
|
'### Progress\n' +
|
|
437
428
|
'- <completed/total>\n' +
|
|
438
429
|
'```\n' +
|
|
430
|
+
'Give every step a short `title` (≤20 chars, e.g. "编写测试" / "修 status bar") that shows in the status bar, and keep the full detail in `content` — the label is for scanning, the content is what must survive compaction. ' +
|
|
439
431
|
'Keep at most one step in_progress, and mark a step completed as soon as its work is done — do not batch updates to the end of the turn. ' +
|
|
440
432
|
'Write each step so a teammate who lost the conversation could pick it up cold: name the file or symbol, the exact change, and the verification, so the plan survives context compaction. ' +
|
|
441
433
|
'plan_update creates notes.md for you when the task warrants it; read_file the full notes.md whenever you need to recover context after compaction. ' +
|
|
442
434
|
'When every step is completed, plan_update settles the plan to `## Done:` automatically. Keep other notes concise and session-specific; use memory for stable cross-session facts.\n' +
|
|
443
435
|
'## Session notes (resident memory)\n' +
|
|
444
436
|
'For non-obvious, lasting-value discoveries — subtle constraints, decisions with downstream impact, open questions blocking a choice, or risks affecting later steps — call `note_append` IMMEDIATELY when you make the discovery. The note is written to the same notes.md and its body is re-injected into the prompt automatically (within a 5k-token budget), surviving compaction so you keep remembering what you found/decided this session. Do NOT use it for routine progress (that is the plan) or stable cross-session facts (that is memory_save). Each call appends one item.');
|
|
445
|
-
const ctxContent = `${agentsImportSection}${
|
|
437
|
+
const ctxContent = `${agentsImportSection}${notepadSection}`.trimEnd();
|
|
446
438
|
if (ctxContent) {
|
|
447
439
|
dynamicParts.push(`## Project context\n${ctxContent}`);
|
|
448
440
|
}
|
|
@@ -477,14 +469,8 @@ export function buildMocodeCorePrompt() {
|
|
|
477
469
|
return full.slice(0, dropStart).trimEnd();
|
|
478
470
|
}
|
|
479
471
|
/**
|
|
480
|
-
* plan
|
|
481
|
-
*
|
|
482
|
-
* 动态拼:false 时不出现 memory_* 工具名,避免 LLM 想调不存在的工具。
|
|
483
|
-
*
|
|
484
|
-
* 注意:已改为 getter(每次访问现拼),让运行时切 /memory_switch 后立即生效。
|
|
485
|
-
* 旧 import `PLAN_MODE_SUFFIX` 路径不变;repl 推荐改用 getPlanModeSuffix()(语义更清晰)。
|
|
486
|
-
* 不能直接 `export const PLAN_MODE_SUFFIX = buildPlanModeSuffix()`:
|
|
487
|
-
* 该表达式在模块初始化时立即求值,而 buildPlanModeSuffix 内部读 config,config 还未求值 → TDZ。
|
|
472
|
+
* plan 模式追加到系统提示末尾的指令。真实工具集合由当前 ToolPolicy snapshot 与
|
|
473
|
+
* PLAN_DISABLED_TOOLS 求交;本 getter 只描述只读行为,不枚举可能不存在的工具名。
|
|
488
474
|
*/
|
|
489
475
|
export function getPlanModeSuffix() {
|
|
490
476
|
return buildPlanModeSuffix();
|
|
@@ -500,19 +486,21 @@ export const config = {
|
|
|
500
486
|
? requireEnv('LLM_API_KEY')
|
|
501
487
|
: (__activePreset?.apiKey ?? requireEnv('LLM_API_KEY')),
|
|
502
488
|
model: llmKeysFromShell.includes('LLM_MODEL')
|
|
503
|
-
?
|
|
489
|
+
? process.env.LLM_MODEL || 'gpt-4o-mini'
|
|
504
490
|
: (__activePreset?.model ?? process.env.LLM_MODEL ?? 'gpt-4o-mini'),
|
|
505
491
|
maxTokens: process.env.MAX_TOKENS ? Number(process.env.MAX_TOKENS) : undefined,
|
|
506
|
-
// 用 getter 而非 buildBasePrompt()
|
|
507
|
-
//
|
|
492
|
+
// 用 getter 而非 buildBasePrompt() 立即求值:对象字面量初始化期间 buildBasePrompt 会读取
|
|
493
|
+
// config.model 等字段,直接调用会触发 TDZ。Getter 也让语言、persona 等运行时变化在下一轮生效。
|
|
508
494
|
get systemPrompt() {
|
|
509
495
|
return buildBasePrompt();
|
|
510
496
|
},
|
|
511
497
|
contextWindowTokens: llmKeysFromShell.includes('CONTEXT_WINDOW_TOKENS')
|
|
512
|
-
?
|
|
513
|
-
:
|
|
514
|
-
|
|
515
|
-
|
|
498
|
+
? Number(process.env.CONTEXT_WINDOW_TOKENS) || DEFAULT_CONTEXT_WINDOW_TOKENS
|
|
499
|
+
: __activePreset?.contextWindow ||
|
|
500
|
+
// 必须用 ||:Number() 永不返回 null/undefined,?? 的右支是死代码;
|
|
501
|
+
// 且环境变量写成非数字时 Number() 得 NaN,?? 会把 NaN 直接放行到 contextWindow。
|
|
502
|
+
Number(process.env.CONTEXT_WINDOW_TOKENS) ||
|
|
503
|
+
DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
516
504
|
includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
|
|
517
505
|
anthropicPromptCache: llmKeysFromShell.includes('ANTHROPIC_PROMPT_CACHE')
|
|
518
506
|
? process.env.ANTHROPIC_PROMPT_CACHE !== 'false'
|
|
@@ -529,14 +517,13 @@ export const config = {
|
|
|
529
517
|
subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
|
|
530
518
|
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || Number(process.env.MAX_STEPS) || 1000,
|
|
531
519
|
frontendToolsEnabled: process.env.MOCODE_FRONTEND_TOOLS_ENABLED === 'true',
|
|
520
|
+
computerUseEnabled: process.env.MOCODE_COMPUTER_USE_ENABLED === 'true',
|
|
532
521
|
mcpEnabled: process.env.MOCODE_MCP_ENABLED !== 'false',
|
|
533
522
|
sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
|
|
534
523
|
searchApiKey: process.env.ANYSEARCH_API_KEY,
|
|
535
524
|
sandboxRoot: process.env.SANDBOX_ROOT || undefined,
|
|
536
525
|
searchBaseUrl: process.env.ANYSEARCH_BASE_URL || 'https://api.anysearch.com',
|
|
537
|
-
maxImageBytes: process.env.MOCODE_MAX_IMAGE_BYTES
|
|
538
|
-
? Number(process.env.MOCODE_MAX_IMAGE_BYTES)
|
|
539
|
-
: undefined,
|
|
526
|
+
maxImageBytes: process.env.MOCODE_MAX_IMAGE_BYTES ? Number(process.env.MOCODE_MAX_IMAGE_BYTES) : undefined,
|
|
540
527
|
theme: process.env.MOCODE_THEME || 'default',
|
|
541
528
|
themeFromShell,
|
|
542
529
|
llmKeysFromShell,
|
|
@@ -596,24 +583,73 @@ export function updateModelConfig(opts) {
|
|
|
596
583
|
process.env.ANTHROPIC_PROMPT_CACHE = opts.anthropicPromptCache ? 'true' : 'false';
|
|
597
584
|
}
|
|
598
585
|
}
|
|
599
|
-
/**
|
|
586
|
+
/** legacy 嵌入路径的 profile 查询;官方主 Agent 不读取。 */
|
|
587
|
+
export function getActiveProfile() {
|
|
588
|
+
return activeProfile;
|
|
589
|
+
}
|
|
590
|
+
/** legacy 嵌入/测试用 profile setter;不再暴露对应 REPL 命令。 */
|
|
591
|
+
export function setActiveProfile(p) {
|
|
592
|
+
if (!isProfileName(p))
|
|
593
|
+
throw new Error(`unknown profile: ${String(p)}`);
|
|
594
|
+
activeProfile = p;
|
|
595
|
+
}
|
|
596
|
+
/** 自动路由 capability gate:只有显式字符串 false 才是硬否决;unset/true 均允许按需选择。 */
|
|
597
|
+
function isRouteCapabilityAllowed(envName) {
|
|
598
|
+
return process.env[envName] !== 'false';
|
|
599
|
+
}
|
|
600
|
+
export function isSubAgentRouteAllowed() {
|
|
601
|
+
return isRouteCapabilityAllowed('MOCODE_SUBAGENT_ENABLED');
|
|
602
|
+
}
|
|
603
|
+
export function isFrontendRouteAllowed() {
|
|
604
|
+
return isRouteCapabilityAllowed('MOCODE_FRONTEND_TOOLS_ENABLED');
|
|
605
|
+
}
|
|
606
|
+
export function isComputerUseRouteAllowed() {
|
|
607
|
+
return isRouteCapabilityAllowed('MOCODE_COMPUTER_USE_ENABLED');
|
|
608
|
+
}
|
|
609
|
+
/** memory-read / memory-write 的自动路由 gate;与 Memory Index 是否注入分开显示。 */
|
|
610
|
+
export function isMemoryRouteAllowed() {
|
|
611
|
+
return isRouteCapabilityAllowed('MEMORY_ENABLED');
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* 子 Agent legacy 可见性:仅供没有 ToolPolicy 的兼容路径。官方 TUI/stdio 使用
|
|
615
|
+
* isSubAgentRouteAllowed() + 每 turn ToolPolicy,不再由 activeProfile 决定。
|
|
616
|
+
*/
|
|
600
617
|
export function isSubAgentEnabled() {
|
|
601
|
-
|
|
618
|
+
if (process.env.MOCODE_SUBAGENT_ENABLED !== undefined)
|
|
619
|
+
return process.env.MOCODE_SUBAGENT_ENABLED === 'true';
|
|
620
|
+
return profileHasGroup(activeProfile, 'subagent');
|
|
621
|
+
}
|
|
622
|
+
/** 自动工具路由执行面的硬否决;未设置或 true 时由当前 ToolPolicy 决定是否暴露 orchestration。 */
|
|
623
|
+
export function isSubAgentHardDisabled() {
|
|
624
|
+
return !isSubAgentRouteAllowed();
|
|
602
625
|
}
|
|
603
|
-
/**
|
|
626
|
+
/** /subagent 的持久化写入口:更新兼容字段与 capability gate;不会把 orchestration 常驻 schema。 */
|
|
604
627
|
export function updateSubAgentConfig(enabled) {
|
|
605
628
|
config.subAgentEnabled = enabled;
|
|
606
629
|
process.env.MOCODE_SUBAGENT_ENABLED = enabled ? 'true' : 'false';
|
|
607
630
|
}
|
|
608
|
-
/**
|
|
631
|
+
/** 前端工具簇的 legacy 可见性;官方自动路由使用 isFrontendRouteAllowed()。 */
|
|
609
632
|
export function isFrontendToolsEnabled() {
|
|
610
|
-
|
|
633
|
+
if (process.env.MOCODE_FRONTEND_TOOLS_ENABLED !== undefined)
|
|
634
|
+
return process.env.MOCODE_FRONTEND_TOOLS_ENABLED === 'true';
|
|
635
|
+
return profileHasGroup(activeProfile, 'frontend');
|
|
611
636
|
}
|
|
612
|
-
/**
|
|
637
|
+
/** /fe 的持久化写入口;官方路径把 false 当硬否决,true 仅允许按需路由。 */
|
|
613
638
|
export function updateFrontendToolsConfig(enabled) {
|
|
614
639
|
config.frontendToolsEnabled = enabled;
|
|
615
640
|
process.env.MOCODE_FRONTEND_TOOLS_ENABLED = enabled ? 'true' : 'false';
|
|
616
641
|
}
|
|
642
|
+
/** Computer Use 的 legacy 可见性;官方自动路由使用 isComputerUseRouteAllowed()。 */
|
|
643
|
+
export function isComputerUseEnabled() {
|
|
644
|
+
if (process.env.MOCODE_COMPUTER_USE_ENABLED !== undefined)
|
|
645
|
+
return process.env.MOCODE_COMPUTER_USE_ENABLED === 'true';
|
|
646
|
+
return profileHasGroup(activeProfile, 'computer');
|
|
647
|
+
}
|
|
648
|
+
/** /cu 的持久化写入口;官方路径把 false 当硬否决,true 仅允许按需路由。 */
|
|
649
|
+
export function updateComputerUseConfig(enabled) {
|
|
650
|
+
config.computerUseEnabled = enabled;
|
|
651
|
+
process.env.MOCODE_COMPUTER_USE_ENABLED = enabled ? 'true' : 'false';
|
|
652
|
+
}
|
|
617
653
|
/** MCP 总开关;关闭时下次启动跳过 MCP 配置读取与服务连接。 */
|
|
618
654
|
export function isMcpEnabled() {
|
|
619
655
|
return config.mcpEnabled;
|
|
@@ -624,12 +660,14 @@ export function updateMcpConfig(enabled) {
|
|
|
624
660
|
process.env.MOCODE_MCP_ENABLED = enabled ? 'true' : 'false';
|
|
625
661
|
}
|
|
626
662
|
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
663
|
+
* Memory Index / 反思开关。它不等于自动路由 gate:官方 ToolPolicy 通过
|
|
664
|
+
* isMemoryRouteAllowed() 判定 memory-read / memory-write 是否可选;这里决定索引和反思是否启用。
|
|
665
|
+
* legacy profile fallback 仍保留给未传 ToolPolicy 的嵌入调用。
|
|
630
666
|
*/
|
|
631
667
|
export function isMemoryEnabled() {
|
|
632
|
-
|
|
668
|
+
if (process.env.MEMORY_ENABLED !== undefined)
|
|
669
|
+
return process.env.MEMORY_ENABLED === 'true';
|
|
670
|
+
return profileHasGroup(activeProfile, 'memory');
|
|
633
671
|
}
|
|
634
672
|
/**
|
|
635
673
|
* .codegraph/ 索引存在性:仅查 cwd 顶层 .codegraph(目录或文件均可,codegraph CLI
|
|
@@ -658,15 +696,9 @@ export function buildCodegraphSection() {
|
|
|
658
696
|
].join('\n');
|
|
659
697
|
}
|
|
660
698
|
/**
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
* 持久化(写 ~/.mocode/config 的 MEMORY_ENABLED 键)由调用方走 writeConfigKeys。
|
|
665
|
-
*
|
|
666
|
-
* 注:开关切换对当前会话的 tool list / 已拼好的 systemPrompt 不会自动重算 —
|
|
667
|
-
* 工具表在 REPL 启动时构建,systemPrompt 在每轮 chat() 拼时按 isMemoryEnabled()
|
|
668
|
-
* 现查现拼(关掉时该轮拼出来的 prompt 即不带 memory_* 段)。所以切换在「下一轮
|
|
669
|
-
* agent 调用」起即时生效,本轮已发出的请求不会回滚。
|
|
699
|
+
* /memory_switch 的写入口:同步 Memory Index 状态与自动路由 capability gate。
|
|
700
|
+
* 内置 memory_* 工具始终注册但默认不暴露;当前已发出的 immutable policy snapshot 不变,
|
|
701
|
+
* 下一真实用户 turn 会按新 gate 重路由并重建系统提示。持久化由调用方写 MEMORY_ENABLED。
|
|
670
702
|
*/
|
|
671
703
|
export function updateMemoryConfig(enabled) {
|
|
672
704
|
config.memoryEnabled = enabled;
|