mocode-ai 1.6.3 → 1.6.5
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 +376 -365
- package/README.zh-CN.md +15 -4
- package/dist/agent/model-turn.js +14 -1
- package/dist/agent/run-coordinator.js +129 -84
- package/dist/agent/spawn.js +38 -5
- package/dist/agent/stages/model-runner.js +1 -1
- package/dist/agent/stages/tool-dispatcher.js +117 -79
- package/dist/agent/work-discipline.js +3 -1
- package/dist/config/index.js +61 -21
- package/dist/config/presets.js +40 -1
- package/dist/config/profiles.js +9 -1
- package/dist/context/clearing.js +53 -0
- package/dist/context/encoders/passthrough.js +2 -2
- package/dist/context/pipeline.js +4 -3
- package/dist/context/relevance.js +58 -13
- package/dist/context/text-search.js +113 -0
- package/dist/llm/index.js +35 -8
- package/dist/llm/providers/anthropic.js +14 -4
- package/dist/llm/reasoning.js +141 -0
- package/dist/memory/reflect.js +1 -1
- package/dist/memory/store.js +31 -28
- package/dist/models/budgets.js +11 -0
- package/dist/models/catalog.js +130 -0
- package/dist/models/map-preset.js +50 -0
- package/dist/models/model-panel.js +227 -0
- package/dist/models/protocol.js +44 -0
- package/dist/models/reasoning-cap.js +88 -0
- package/dist/models/search.js +58 -0
- package/dist/models/types.js +8 -0
- package/dist/repl/commands/effort.js +81 -0
- package/dist/repl/commands/model-actions.js +138 -0
- package/dist/repl/commands/model-catalog.js +67 -0
- package/dist/repl/commands/model-panel-ui.js +107 -0
- package/dist/repl/commands/model.js +17 -0
- package/dist/repl/commands/registry.js +6 -0
- package/dist/repl/commands/session.js +54 -0
- package/dist/repl/commands/stats.js +28 -0
- package/dist/repl/commands.js +3 -0
- package/dist/repl/runtime.js +5 -1
- package/dist/repl/status-bar.js +4 -2
- package/dist/rollback/store.js +17 -0
- package/dist/runtime/runtime.js +31 -1
- package/dist/session/compact.js +84 -3
- package/dist/session/index.js +2 -0
- package/dist/session/retention.js +297 -0
- package/dist/session/scheduler.js +30 -3
- package/dist/session/store.js +108 -6
- package/dist/session/usage-stats.js +90 -0
- package/dist/skills/activation.js +2 -0
- package/dist/skills/builtin-skills.js +153 -0
- package/dist/skills/discover.js +3 -1
- package/dist/skills/runner.js +4 -1
- package/dist/tools/builtins/edit-file.js +2 -4
- package/dist/tools/builtins/grep.js +2 -4
- package/dist/tools/builtins/index.js +4 -0
- package/dist/tools/builtins/read-file.js +21 -5
- package/dist/tools/builtins/session-search.js +33 -0
- package/dist/tools/builtins/task.js +2 -0
- package/dist/tools/constants.js +8 -0
- package/dist/tools/policy.js +1 -1
- package/dist/tools/read-dedup.js +62 -0
- package/dist/tools/tool-runtime.js +1 -0
- package/dist/ui/batch.js +109 -103
- package/dist/ui/fuzzy-picker.js +227 -0
- package/dist/ui/hierarchical-picker.js +302 -0
- package/dist/ui/intervention.js +11 -1
- package/dist/ui/layout-internal/statusbar.js +18 -7
- package/dist/ui/render.js +2 -0
- package/package.json +3 -2
|
@@ -46,6 +46,7 @@ class LegacyCompatibleToolDispatcher {
|
|
|
46
46
|
callId: call.id,
|
|
47
47
|
allowedToolNames: request.currentAllowedToolNames(),
|
|
48
48
|
delegation: request.delegation(),
|
|
49
|
+
readDedup: request.readDedup,
|
|
49
50
|
...(hint ? { argumentErrorHint: hint } : {}),
|
|
50
51
|
...(onLockAcquired ? { onLockAcquired } : {}),
|
|
51
52
|
});
|
|
@@ -76,95 +77,132 @@ class LegacyCompatibleToolDispatcher {
|
|
|
76
77
|
argumentSummary: argumentSummaries[index],
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
|
-
const
|
|
80
|
+
const controlIndexes = [];
|
|
81
|
+
const otherIndexes = [];
|
|
82
|
+
calls.forEach((call, index) => (call.name === ADD_TOOL_GROUPS_TOOL_NAME ? controlIndexes : otherIndexes).push(index));
|
|
83
|
+
const hasRouteBarrier = controlIndexes.length > 0;
|
|
80
84
|
if (hasRouteBarrier) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
// 同批的非控制调用全部是「未被禁用的并行安全(只读)工具」时(纯 solo 时空集也满足),
|
|
86
|
+
// 允许它们与扩容同批:先并发执行只读调用,再应用扩容。只读结果本 step 即得,新增 schema
|
|
87
|
+
// 下一 step 生效——不必为扩容空耗一轮。只要混有写/执行等非并行工具(或某只读调用已被
|
|
88
|
+
// 当前 snapshot 拒绝),即退回整批保守拒绝。
|
|
89
|
+
const safeReadonlyBatch = otherIndexes.every((index) => isParallelTool(calls[index].name, toolRuntime) && !request.isDenied(calls[index].name));
|
|
90
|
+
if (safeReadonlyBatch) {
|
|
91
|
+
// 所有 header 按调用原序先发(渲染侧据此建组容器),只读批的 header 必须先于 execute。
|
|
92
|
+
for (let index = 0; index < calls.length; index++) {
|
|
93
|
+
request.onEvent({ type: 'header', call: calls[index] });
|
|
94
|
+
}
|
|
95
|
+
if (otherIndexes.length > 0) {
|
|
96
|
+
request.onEvent({ type: 'start', tool: calls[otherIndexes[0]].name });
|
|
97
|
+
const startedReadonly = otherIndexes.map((index) => execute(calls[index]));
|
|
98
|
+
for (let offset = 0; offset < otherIndexes.length; offset++) {
|
|
99
|
+
const index = otherIndexes[offset];
|
|
100
|
+
const outcome = await startedReadonly[offset];
|
|
101
|
+
record(index, outcome);
|
|
102
|
+
executionEvents(index, parseArgs(calls[index].arguments), outcome);
|
|
103
|
+
resultEvent(index, outcome, null);
|
|
104
|
+
}
|
|
105
|
+
request.onEvent({ type: 'done' });
|
|
106
|
+
}
|
|
107
|
+
// 控制调用(header 已发):逐个校验并应用扩容;solo 与同批语义一致。
|
|
108
|
+
for (const index of controlIndexes) {
|
|
109
|
+
const call = calls[index];
|
|
110
|
+
const parsed = parseArgs(call.arguments);
|
|
111
|
+
let outcome;
|
|
112
|
+
if (request.isDenied(call.name)) {
|
|
113
|
+
outcome = {
|
|
114
|
+
status: 'denied',
|
|
115
|
+
code: 'TOOL_DISABLED',
|
|
116
|
+
retryable: false,
|
|
117
|
+
output: `错误:当前 tool policy snapshot 不允许调用 ${call.name}。`,
|
|
118
|
+
changedFiles: [],
|
|
119
|
+
durationMs: 0,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
else if (!request.expandToolGroups) {
|
|
123
|
+
outcome = {
|
|
124
|
+
status: 'denied',
|
|
125
|
+
code: 'TOOL_DISABLED',
|
|
126
|
+
retryable: false,
|
|
127
|
+
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
128
|
+
changedFiles: [],
|
|
129
|
+
durationMs: 0,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
else if (!parsed ||
|
|
133
|
+
!Array.isArray(parsed.groups) ||
|
|
134
|
+
parsed.groups.length === 0 ||
|
|
135
|
+
typeof parsed.reason !== 'string' ||
|
|
136
|
+
!parsed.reason.trim()) {
|
|
137
|
+
outcome = {
|
|
138
|
+
status: 'error',
|
|
139
|
+
code: 'INVALID_ARGUMENTS',
|
|
140
|
+
retryable: false,
|
|
141
|
+
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
142
|
+
changedFiles: [],
|
|
143
|
+
durationMs: 0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const expansion = request.expandToolGroups(parsed.groups, parsed.reason);
|
|
148
|
+
const succeeded = expansion.added.length > 0;
|
|
149
|
+
const details = [
|
|
150
|
+
succeeded
|
|
151
|
+
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
152
|
+
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
153
|
+
expansion.implied.length > 0 ? `Implied groups also activated: ${expansion.implied.join(', ')}.` : '',
|
|
154
|
+
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
155
|
+
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
156
|
+
]
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join('\n');
|
|
159
|
+
outcome = {
|
|
160
|
+
status: succeeded ? 'success' : 'error',
|
|
161
|
+
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
162
|
+
retryable: false,
|
|
163
|
+
output: details,
|
|
164
|
+
changedFiles: [],
|
|
165
|
+
durationMs: 0,
|
|
166
|
+
};
|
|
167
|
+
request.onEvent({
|
|
168
|
+
type: 'route_expand',
|
|
169
|
+
fromVersion: request.policy.toolPolicy?.version,
|
|
170
|
+
expansion,
|
|
171
|
+
requestedGroups: parsed.groups,
|
|
172
|
+
reason: parsed.reason,
|
|
173
|
+
status: outcome.status,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
record(index, outcome);
|
|
177
|
+
request.onEvent({ type: 'host_outcome', call, parsed: parsed ?? {}, outcome });
|
|
178
|
+
resultEvent(index, outcome, null);
|
|
179
|
+
traceEnd(index, outcome);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// 不安全批(混有写/执行等非并行工具):不扩容、不执行任何普通工具,逐 call 按原序配对拒绝结果。
|
|
184
|
+
for (let index = 0; index < calls.length; index++) {
|
|
185
|
+
const call = calls[index];
|
|
186
|
+
request.onEvent({ type: 'header', call });
|
|
88
187
|
const isControl = call.name === ADD_TOOL_GROUPS_TOOL_NAME;
|
|
89
|
-
|
|
188
|
+
const isReadonly = isParallelTool(call.name, toolRuntime);
|
|
189
|
+
const outcome = {
|
|
90
190
|
status: 'denied',
|
|
91
191
|
code: isControl ? 'INVALID_ARGUMENTS' : 'TOOL_DISABLED',
|
|
92
192
|
retryable: false,
|
|
93
193
|
output: isControl
|
|
94
|
-
? '错误:add_tool_groups
|
|
95
|
-
:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
else if (request.isDenied(call.name)) {
|
|
101
|
-
outcome = {
|
|
102
|
-
status: 'denied',
|
|
103
|
-
code: 'TOOL_DISABLED',
|
|
104
|
-
retryable: false,
|
|
105
|
-
output: `错误:当前 tool policy snapshot 不允许调用 ${call.name}。`,
|
|
194
|
+
? '错误:add_tool_groups 只能单独调用,或与只读工具(read_file/glob/grep/web 等)同批;本次没有扩容。'
|
|
195
|
+
: isReadonly
|
|
196
|
+
? `错误:同一响应包含 add_tool_groups,工具 ${call.name} 未执行。请在下一 step 重试。`
|
|
197
|
+
: `错误:add_tool_groups 不能与写/执行工具 ${call.name} 同批;请先完成扩容,再在下一 step 调用 ${call.name}。`,
|
|
106
198
|
changedFiles: [],
|
|
107
199
|
durationMs: 0,
|
|
108
200
|
};
|
|
201
|
+
record(index, outcome);
|
|
202
|
+
request.onEvent({ type: 'host_outcome', call, parsed: parseArgs(call.arguments) ?? {}, outcome });
|
|
203
|
+
resultEvent(index, outcome, null);
|
|
204
|
+
traceEnd(index, outcome);
|
|
109
205
|
}
|
|
110
|
-
else if (!request.expandToolGroups) {
|
|
111
|
-
outcome = {
|
|
112
|
-
status: 'denied',
|
|
113
|
-
code: 'TOOL_DISABLED',
|
|
114
|
-
retryable: false,
|
|
115
|
-
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
116
|
-
changedFiles: [],
|
|
117
|
-
durationMs: 0,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
else if (!parsed ||
|
|
121
|
-
!Array.isArray(parsed.groups) ||
|
|
122
|
-
parsed.groups.length === 0 ||
|
|
123
|
-
typeof parsed.reason !== 'string' ||
|
|
124
|
-
!parsed.reason.trim()) {
|
|
125
|
-
outcome = {
|
|
126
|
-
status: 'error',
|
|
127
|
-
code: 'INVALID_ARGUMENTS',
|
|
128
|
-
retryable: false,
|
|
129
|
-
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
130
|
-
changedFiles: [],
|
|
131
|
-
durationMs: 0,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
const expansion = request.expandToolGroups(parsed.groups, parsed.reason);
|
|
136
|
-
const succeeded = expansion.added.length > 0;
|
|
137
|
-
const details = [
|
|
138
|
-
succeeded
|
|
139
|
-
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
140
|
-
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
141
|
-
expansion.implied.length > 0 ? `Implied groups also activated: ${expansion.implied.join(', ')}.` : '',
|
|
142
|
-
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
143
|
-
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
144
|
-
]
|
|
145
|
-
.filter(Boolean)
|
|
146
|
-
.join('\n');
|
|
147
|
-
outcome = {
|
|
148
|
-
status: succeeded ? 'success' : 'error',
|
|
149
|
-
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
150
|
-
retryable: false,
|
|
151
|
-
output: details,
|
|
152
|
-
changedFiles: [],
|
|
153
|
-
durationMs: 0,
|
|
154
|
-
};
|
|
155
|
-
request.onEvent({
|
|
156
|
-
type: 'route_expand',
|
|
157
|
-
fromVersion: request.policy.toolPolicy?.version,
|
|
158
|
-
expansion,
|
|
159
|
-
requestedGroups: parsed.groups,
|
|
160
|
-
reason: parsed.reason,
|
|
161
|
-
status: outcome.status,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
record(index, outcome);
|
|
165
|
-
request.onEvent({ type: 'host_outcome', call, parsed: parsed ?? {}, outcome });
|
|
166
|
-
resultEvent(index, outcome, null);
|
|
167
|
-
traceEnd(index, outcome);
|
|
168
206
|
}
|
|
169
207
|
}
|
|
170
208
|
let index = hasRouteBarrier ? calls.length : 0;
|
|
@@ -41,9 +41,11 @@ Call \`ask_human\` before coding only when repository evidence cannot resolve a
|
|
|
41
41
|
3. multiple reasonable options that materially change product behavior;
|
|
42
42
|
4. the request itself admits two or more materially different readings that lead to different deliverables (do not silently pick one and guess).
|
|
43
43
|
|
|
44
|
+
When the request is ambiguous, vague, or missing key information, first load the \`brainstorming\` skill and clarify the need through a short dialogue (one question at a time) before starting work.
|
|
45
|
+
|
|
44
46
|
For naming and implementation details, follow repository precedent and choose the safest reversible default. Disclose any consequential assumption.
|
|
45
47
|
|
|
46
|
-
Budget: at most 2 \`ask_human\` calls per turn
|
|
48
|
+
Budget: during requirement clarification (the \`brainstorming\` workflow) ask as many questions as genuinely advance understanding; during execution after requirements are clear, use at most 2 \`ask_human\` calls per turn, then fall back to the safest reversible default and disclose it in the final reply.`;
|
|
47
49
|
/** Advisory guidance shared by main and sub-agents. */
|
|
48
50
|
export function buildWorkDisciplineSection(_modelFamily) {
|
|
49
51
|
return `${CORE_SECTION}\n\n${ASK_WHITELIST_SECTION}`;
|
package/dist/config/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
|
+
import { parseReasoningEffort, getScopedEffort } from '../llm/reasoning.js';
|
|
6
|
+
import { getActiveSkill } from '../skills/activation.js';
|
|
5
7
|
import { getCurrentSessionId } from '../session/state.js';
|
|
6
8
|
import { getNotesFilePath, extractActiveNotesSections } from '../session/notes.js';
|
|
7
9
|
import { buildGuiActionsSection } from '../session/gui-actions.js';
|
|
@@ -43,7 +45,7 @@ function loadEnvFiles() {
|
|
|
43
45
|
process.env[k] = v;
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
// 在 loadEnvFiles
|
|
48
|
+
// 在 loadEnvFiles 回填前先捕获:MOCODE_THEME / MOCODE_LANGUAGE 是否由 shell 显式 export(决定后续优先级提示)。
|
|
47
49
|
const themeFromShell = process.env.MOCODE_THEME !== undefined;
|
|
48
50
|
export const languageFromShell = process.env.MOCODE_LANGUAGE !== undefined;
|
|
49
51
|
// 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
|
|
@@ -56,6 +58,7 @@ const LLM_ENV_KEYS = [
|
|
|
56
58
|
'LLM_MODEL',
|
|
57
59
|
'CONTEXT_WINDOW_TOKENS',
|
|
58
60
|
'ANTHROPIC_PROMPT_CACHE',
|
|
61
|
+
'REASONING_EFFORT',
|
|
59
62
|
];
|
|
60
63
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 256000;
|
|
61
64
|
const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
|
|
@@ -492,29 +495,20 @@ ${buildVoiceSection()}
|
|
|
492
495
|
- Stop immediately when no more tools are needed; give conclusions directly.
|
|
493
496
|
- **Do not stop prematurely during exploration**: if you started investigating but haven't gathered enough information to answer the user's question, keep calling tools. Only stop when you have sufficient evidence or hit a dead end.
|
|
494
497
|
- **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
|
|
495
|
-
- 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
|
|
498
|
+
- 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.
|
|
499
|
+
- Definition of done: never declare a multi-feature request finished just because the code exists. Each distinct user request listed in the plan must be actually verified (the relevant check run / observed result), not merely implemented; mark plan steps \`[x]\` only after that verification. If the user supplied an acceptance list, every item on it must pass. Leave unverified items explicitly listed as pending instead of collapsing them into "done".`;
|
|
496
500
|
// 动态段(置于末尾):AGENTS.md 项目记忆 + notepad 索引/说明。工具簇特定指导由
|
|
497
501
|
// ToolPolicyController.reminder() 按当前 turn 的 route 注入,避免旧全局 profile 与真实 schema 分裂。
|
|
498
502
|
// 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
|
|
499
503
|
const dynamicParts = [];
|
|
500
|
-
// 会话级私有尾段(子 agent 切片会丢弃)
|
|
504
|
+
// 会话级私有尾段(子 agent 切片会丢弃)。只放一句指针:plan/note 的完整规则由
|
|
505
|
+
// plan_update / note_append 的 description 承载(两工具在常驻面,调用时一定可见),
|
|
506
|
+
// 避免把规则在系统提示里每请求预付第三遍。**标题 '## Session state' 必须保留**:
|
|
507
|
+
// buildMocodeCorePrompt 靠 MARKER_DROPPABLE_SECTION 给子 agent 切片。
|
|
501
508
|
dynamicParts.push(`## Session state (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
|
|
502
|
-
'
|
|
503
|
-
'
|
|
504
|
-
'
|
|
505
|
-
'## Plan: <title>\n' +
|
|
506
|
-
'Goal: <outcome>\n' +
|
|
507
|
-
'### Steps\n' +
|
|
508
|
-
'- [ ] 1. **<short label, ≤20 chars>** — <self-contained step: target file/symbol, the change, and how to verify>\n' +
|
|
509
|
-
'### Progress\n' +
|
|
510
|
-
'- <completed/total>\n' +
|
|
511
|
-
'```\n' +
|
|
512
|
-
'Each step: short `title` (≤20 chars, e.g. "编写测试" / "修 status bar", shown in the status bar) + self-contained `content` (target file/symbol, exact change, verification — readable cold, without this conversation). ' +
|
|
513
|
-
'Keep at most one step in_progress; mark a step completed as soon as its work is done, not batched to the end of the turn. ' +
|
|
514
|
-
'plan_update creates notes.md on demand and settles the plan to `## Done:` when all steps complete; run read_file on the full notes.md to recover context after compaction. ' +
|
|
515
|
-
'Keep other notes concise and session-specific; use memory for stable cross-session facts.\n' +
|
|
516
|
-
'## Session notes (resident memory)\n' +
|
|
517
|
-
'For lasting-value discoveries — subtle constraints, decisions with downstream impact, open questions, or risks — call `note_append` IMMEDIATELY when you make the discovery. Notes land in the same notes.md and are re-injected into the prompt automatically (5k-token budget), surviving compaction. Do NOT use for routine progress (that is the plan) or stable cross-session facts (that is memory_save). Each call appends one item.');
|
|
509
|
+
'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`. ' +
|
|
510
|
+
'Both write notes.md, which survives compaction — run read_file on it to recover context after compaction. ' +
|
|
511
|
+
'Their formats and rules live in each tool\u2019s description; do not hand-edit the files.');
|
|
518
512
|
// 日期段:模型需要知道今天才能判断 freshness(web 搜索、版本时效)。只随天变化,
|
|
519
513
|
// 不破坏会话内前缀缓存;置于 '## Session state' 之前,子 agent 切片仍保留(无害且有用)。
|
|
520
514
|
const todaySection = buildTodaySection();
|
|
@@ -592,10 +586,17 @@ export const config = {
|
|
|
592
586
|
? process.env.ANTHROPIC_PROMPT_CACHE !== 'false'
|
|
593
587
|
: (__activePreset?.anthropicPromptCache ?? process.env.ANTHROPIC_PROMPT_CACHE !== 'false'),
|
|
594
588
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
595
|
-
|
|
596
|
-
|
|
589
|
+
reasoningEffort: llmKeysFromShell.includes('REASONING_EFFORT')
|
|
590
|
+
? (parseReasoningEffort(process.env.REASONING_EFFORT) ?? 'auto')
|
|
591
|
+
: (__activePreset?.reasoningEffort ?? parseReasoningEffort(process.env.REASONING_EFFORT) ?? 'auto'),
|
|
592
|
+
compactFork: process.env.MOCODE_COMPACT_FORK !== 'false',
|
|
593
|
+
readDedup: process.env.MOCODE_READ_DEDUP !== 'false',
|
|
594
|
+
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
|
|
595
|
+
contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE !== 'false',
|
|
597
596
|
contextLifecycle: process.env.MOCODE_LIFECYCLE !== 'false',
|
|
598
597
|
contextBudget: process.env.MOCODE_BUDGET_SCHEDULER !== 'false',
|
|
598
|
+
lowPressureRatio: Math.min(0.95, Math.max(0.1, Number(process.env.MOCODE_LOW_PRESSURE_RATIO) || 0.6)),
|
|
599
|
+
toolClearing: process.env.MOCODE_TOOL_CLEARING !== 'false',
|
|
599
600
|
autoReflect: process.env.AUTO_REFLECT === 'true',
|
|
600
601
|
memoryEnabled: process.env.MEMORY_ENABLED === 'true',
|
|
601
602
|
reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
|
|
@@ -603,6 +604,7 @@ export const config = {
|
|
|
603
604
|
subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
|
|
604
605
|
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || Number(process.env.MAX_STEPS) || 1000,
|
|
605
606
|
subAgentConcurrency: Math.max(1, Number(process.env.SUB_AGENT_CONCURRENCY) || 5),
|
|
607
|
+
subAgentMaxDepth: Math.max(1, Number(process.env.SUB_AGENT_MAX_DEPTH) || 3),
|
|
606
608
|
frontendToolsEnabled: process.env.MOCODE_FRONTEND_TOOLS_ENABLED === 'true',
|
|
607
609
|
computerUseEnabled: process.env.MOCODE_COMPUTER_USE_ENABLED === 'true',
|
|
608
610
|
mcpEnabled: process.env.MOCODE_MCP_ENABLED !== 'false',
|
|
@@ -646,6 +648,38 @@ export function pinSessionModel() {
|
|
|
646
648
|
export function getActiveModel() {
|
|
647
649
|
return sessionModel ?? config.model;
|
|
648
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* 会话级思考强度钉死(P3),与 sessionModel 同构:窗口启动 pinSessionEffort() 捕获,
|
|
653
|
+
* /effort 显式修改同步更新钉死值;其它窗口的设置互不影响。
|
|
654
|
+
*/
|
|
655
|
+
let sessionEffort = null;
|
|
656
|
+
/** REPL 启动时调用一次。 */
|
|
657
|
+
export function pinSessionEffort() {
|
|
658
|
+
sessionEffort = config.reasoningEffort;
|
|
659
|
+
}
|
|
660
|
+
/** 运行中 agent 实际使用的思考强度。 */
|
|
661
|
+
export function getActiveEffort() {
|
|
662
|
+
return sessionEffort ?? config.reasoningEffort;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* 生效思考强度统一解析(P3 §4.8):
|
|
666
|
+
* 显式 per-request 参数(compact/reflect 的 low)> fork ALS scope(skill 子树)
|
|
667
|
+
* > inline 激活 skill 的 effort > 会话级。
|
|
668
|
+
* shell env 显式设置 REASONING_EFFORT 时,skill effort 不参与(对齐 Claude Code:env 最高)。
|
|
669
|
+
*/
|
|
670
|
+
export function effectiveReasoningEffort(explicit) {
|
|
671
|
+
if (explicit !== undefined)
|
|
672
|
+
return explicit;
|
|
673
|
+
const scoped = getScopedEffort();
|
|
674
|
+
if (scoped)
|
|
675
|
+
return scoped;
|
|
676
|
+
if (!config.llmKeysFromShell.includes('REASONING_EFFORT')) {
|
|
677
|
+
const skillEffort = getActiveSkill()?.effort;
|
|
678
|
+
if (skillEffort)
|
|
679
|
+
return skillEffort;
|
|
680
|
+
}
|
|
681
|
+
return getActiveEffort();
|
|
682
|
+
}
|
|
649
683
|
/**
|
|
650
684
|
* 运行时更新模型相关配置(/model 命令调)。
|
|
651
685
|
* - 更新 config 对象字段(即时生效:chat() 读 config.model,reconfigureClient 读 config.baseURL/apiKey)。
|
|
@@ -683,6 +717,12 @@ export function updateModelConfig(opts) {
|
|
|
683
717
|
config.anthropicPromptCache = opts.anthropicPromptCache;
|
|
684
718
|
process.env.ANTHROPIC_PROMPT_CACHE = opts.anthropicPromptCache ? 'true' : 'false';
|
|
685
719
|
}
|
|
720
|
+
if (opts.reasoningEffort !== undefined) {
|
|
721
|
+
config.reasoningEffort = opts.reasoningEffort;
|
|
722
|
+
if (sessionEffort !== null)
|
|
723
|
+
sessionEffort = opts.reasoningEffort;
|
|
724
|
+
process.env.REASONING_EFFORT = opts.reasoningEffort;
|
|
725
|
+
}
|
|
686
726
|
}
|
|
687
727
|
/** legacy 嵌入路径的 profile 查询;官方主 Agent 不读取。 */
|
|
688
728
|
export function getActiveProfile() {
|
package/dist/config/presets.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { parseReasoningEffort } from '../llm/reasoning.js';
|
|
4
5
|
/**
|
|
5
6
|
* 多模型预设(`/model save <name>` 保存的命名配置)的纯 I/O 叶子。
|
|
6
7
|
*
|
|
@@ -78,6 +79,41 @@ export function parsePreset(raw) {
|
|
|
78
79
|
}
|
|
79
80
|
const provider = obj.provider === 'anthropic' ? 'anthropic' : 'openai';
|
|
80
81
|
const anthropicPromptCache = provider === 'anthropic' && obj.anthropicPromptCache !== false;
|
|
82
|
+
// reasoningEffort 缺省 = auto;显式写了非法值按现有校验风格报错(坏文件由 listPresets 跳过)。
|
|
83
|
+
let reasoningEffort;
|
|
84
|
+
if (obj.reasoningEffort !== undefined) {
|
|
85
|
+
reasoningEffort = parseReasoningEffort(obj.reasoningEffort);
|
|
86
|
+
if (!reasoningEffort)
|
|
87
|
+
throw new Error(`预设 ${name}: reasoningEffort 非法(off|low|medium|high|auto)`);
|
|
88
|
+
}
|
|
89
|
+
// ── 目录可选字段:只在「存在且类型正确」时采纳,缺失/错误即 undefined,不报错 ──
|
|
90
|
+
const extra = {};
|
|
91
|
+
if (typeof obj.catalogProvider === 'string' && obj.catalogProvider)
|
|
92
|
+
extra.catalogProvider = obj.catalogProvider;
|
|
93
|
+
if (typeof obj.catalogModel === 'string' && obj.catalogModel)
|
|
94
|
+
extra.catalogModel = obj.catalogModel;
|
|
95
|
+
if (obj.capabilities && typeof obj.capabilities === 'object') {
|
|
96
|
+
const c = obj.capabilities;
|
|
97
|
+
const caps = {};
|
|
98
|
+
if (typeof c.reasoning === 'boolean')
|
|
99
|
+
caps.reasoning = c.reasoning;
|
|
100
|
+
if (typeof c.toolCall === 'boolean')
|
|
101
|
+
caps.toolCall = c.toolCall;
|
|
102
|
+
if (typeof c.attachment === 'boolean')
|
|
103
|
+
caps.attachment = c.attachment;
|
|
104
|
+
if (Array.isArray(c.reasoningOptions))
|
|
105
|
+
caps.reasoningOptions = c.reasoningOptions;
|
|
106
|
+
extra.capabilities = caps;
|
|
107
|
+
}
|
|
108
|
+
if (obj.pricing && typeof obj.pricing === 'object') {
|
|
109
|
+
const p = obj.pricing;
|
|
110
|
+
const pricing = {};
|
|
111
|
+
for (const k of ['input', 'output', 'cacheRead']) {
|
|
112
|
+
if (typeof p[k] === 'number' && Number.isFinite(p[k]))
|
|
113
|
+
pricing[k] = p[k];
|
|
114
|
+
}
|
|
115
|
+
extra.pricing = pricing;
|
|
116
|
+
}
|
|
81
117
|
return {
|
|
82
118
|
name,
|
|
83
119
|
provider,
|
|
@@ -86,6 +122,8 @@ export function parsePreset(raw) {
|
|
|
86
122
|
model,
|
|
87
123
|
contextWindow: Math.floor(contextWindow),
|
|
88
124
|
anthropicPromptCache,
|
|
125
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
126
|
+
...extra,
|
|
89
127
|
};
|
|
90
128
|
}
|
|
91
129
|
/** 读单个预设;不存在抛错。 */
|
|
@@ -217,7 +255,8 @@ export function migrateCurrentToPreset(input) {
|
|
|
217
255
|
p.apiKey === input.apiKey &&
|
|
218
256
|
p.model === input.model &&
|
|
219
257
|
p.contextWindow === input.contextWindow &&
|
|
220
|
-
p.anthropicPromptCache === anthropicPromptCache
|
|
258
|
+
p.anthropicPromptCache === anthropicPromptCache &&
|
|
259
|
+
(p.reasoningEffort ?? 'auto') === 'auto');
|
|
221
260
|
if (dup)
|
|
222
261
|
return null;
|
|
223
262
|
// 'default' 已被占 → 用户已显式起过预设,无需老数据迁入;返回 null 让调用方跳过即可。
|
package/dist/config/profiles.js
CHANGED
|
@@ -19,7 +19,15 @@ export const TOOL_GROUPS = {
|
|
|
19
19
|
web: ['web_search', 'web_fetch'],
|
|
20
20
|
frontend: ['browser', 'screenshot'],
|
|
21
21
|
computer: ['computer'],
|
|
22
|
-
memory: [
|
|
22
|
+
memory: [
|
|
23
|
+
'memory_save',
|
|
24
|
+
'memory_search',
|
|
25
|
+
'memory_list',
|
|
26
|
+
'memory_update',
|
|
27
|
+
'memory_forget',
|
|
28
|
+
'memory_graph',
|
|
29
|
+
'session_search',
|
|
30
|
+
],
|
|
23
31
|
subagent: ['sub-agent'],
|
|
24
32
|
};
|
|
25
33
|
// ── LLM 自动工具路由 ──────────────────────────────────────────────────────
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Tool-result clearing: 可重取结果的低成本清除(Anthropic 三原语之一)。
|
|
2
|
+
//
|
|
3
|
+
// 与 relevance pruner 的区别:pruner 只在有「精确的新替代」时 stub;clearing 更宽——
|
|
4
|
+
// 冷区中来自可重取工具的结果,无论有没有新替代,内容都可丢弃(需要时重新调用即可),
|
|
5
|
+
// 只保留「调用发生过」的 tombstone。不删消息、不改 tool_call_id 配对。
|
|
6
|
+
//
|
|
7
|
+
// 在低压阶段(60%)运行:零 LLM 成本、不依赖摘要质量。热区(最近 hotTurnWindow 个
|
|
8
|
+
// user turn)不动。
|
|
9
|
+
import { toText } from './utils.js';
|
|
10
|
+
/** 结果可随时重取的工具:纯读、无副作用。 */
|
|
11
|
+
export const REFETCHABLE_TOOLS = new Set(['read_file', 'grep', 'glob', 'web_search', 'web_fetch']);
|
|
12
|
+
/** Tombstone 前缀,与 relevance stub 同属 ⌦[ 家族,其余子系统据此识别已清除内容。 */
|
|
13
|
+
const CLEARED_PREFIX = '⌦[已清除:';
|
|
14
|
+
/** 回溯 tool_call_id → 产生该结果的工具名(同 relevance.ts callAt 的最小版本)。 */
|
|
15
|
+
function toolNameAt(history, idx) {
|
|
16
|
+
const tcId = history[idx]?.tool_call_id;
|
|
17
|
+
if (!tcId)
|
|
18
|
+
return null;
|
|
19
|
+
for (let j = idx - 1; j >= 1; j--) {
|
|
20
|
+
if (history[j].role !== 'assistant')
|
|
21
|
+
continue;
|
|
22
|
+
const calls = history[j].tool_calls;
|
|
23
|
+
const hit = calls?.find((tc) => tc?.id === tcId);
|
|
24
|
+
if (hit?.function?.name)
|
|
25
|
+
return hit.function.name;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 清除冷区可重取工具的结果内容。
|
|
31
|
+
* @param coldBoundary 仅处理 index < coldBoundary 的消息(热区保留)。
|
|
32
|
+
* @returns 被清除的消息数。
|
|
33
|
+
*/
|
|
34
|
+
export function clearRetrievableResults(history, coldBoundary) {
|
|
35
|
+
let cleared = 0;
|
|
36
|
+
const end = Math.min(coldBoundary, history.length);
|
|
37
|
+
for (let idx = 1; idx < end; idx++) {
|
|
38
|
+
const message = history[idx];
|
|
39
|
+
if (message.role !== 'tool' || !message.tool_call_id)
|
|
40
|
+
continue;
|
|
41
|
+
const content = toText(message.content);
|
|
42
|
+
if (!content || content.startsWith('⌦['))
|
|
43
|
+
continue;
|
|
44
|
+
const name = toolNameAt(history, idx);
|
|
45
|
+
if (!name || !REFETCHABLE_TOOLS.has(name))
|
|
46
|
+
continue;
|
|
47
|
+
message.content =
|
|
48
|
+
`${CLEARED_PREFIX}${name}] 原结果 ${content.length} 字符已清除(可重新调用获取)` +
|
|
49
|
+
` · id …${message.tool_call_id.slice(-6)}⌫`;
|
|
50
|
+
cleared++;
|
|
51
|
+
}
|
|
52
|
+
return cleared;
|
|
53
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 兜底 encoder:identity,原样返回。
|
|
3
3
|
* - classifier 未命中任何 kind(返回 'passthrough')时用。
|
|
4
|
-
* - pipeline 总开关关闭(MOCODE_CONTEXT_OPTIMIZE=false)时,所有 kind 都走它 →
|
|
5
|
-
* -
|
|
4
|
+
* - pipeline 总开关关闭(MOCODE_CONTEXT_OPTIMIZE=false)时,所有 kind 都走它 → 与不启用该阶段时的行为逐字节一致。
|
|
5
|
+
* - 未注册任何 encoder(仅 passthrough)→ 全链路零改写。
|
|
6
6
|
* - 任何 encoder 报错时,pipeline catch 后回落到它(传原 output)。
|
|
7
7
|
*
|
|
8
8
|
* 永不抛错:output 可能是任意字符串(含 ANSI / 多行 / 非法 UTF-8 片段),identity 直接返回,无解析风险。
|
package/dist/context/pipeline.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Normal tool insertion does not call this module: agent/core stores the raw
|
|
4
4
|
// result after only capToolResultForHistory(). The pressure scheduler invokes
|
|
5
|
-
// this encoder for Cold logs and retrievable searches
|
|
6
|
-
// enabled
|
|
5
|
+
// this encoder for Cold logs and retrievable searches; the switch defaults to
|
|
6
|
+
// enabled and can be turned off via MOCODE_CONTEXT_OPTIMIZE=false. Encoder
|
|
7
|
+
// failures always fall back to the raw hard-capped result.
|
|
7
8
|
//
|
|
8
9
|
// This does not alter tool schemas, execution, tool_call_id pairing, or TUI
|
|
9
10
|
// rendering; it only provides a pressure-stage representation transform.
|
|
@@ -53,7 +54,7 @@ function budgetFor(name) {
|
|
|
53
54
|
*/
|
|
54
55
|
export function optimizeToolResult(name, output, argsRaw, context = {}) {
|
|
55
56
|
boot();
|
|
56
|
-
//
|
|
57
|
+
// Opt-out: defaults on; when disabled, normal history is raw apart from the hard cap.
|
|
57
58
|
if (!config.contextOptimize) {
|
|
58
59
|
return capToolResultForHistory(name, output);
|
|
59
60
|
}
|