mocode-ai 1.5.9 → 1.5.11
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/model-turn.js +10 -1
- package/dist/agent/run-coordinator.js +93 -1
- package/dist/agent/stages/history-manager.js +13 -0
- package/dist/agent/stages/tool-dispatcher.js +66 -1
- package/dist/agent/tool-helpers.js +13 -1
- package/dist/agent/tool-turn.js +7 -1
- package/dist/config/index.js +107 -7
- package/dist/context/vision-window.js +220 -0
- package/dist/i18n/index.js +42 -0
- package/dist/llm/providers/anthropic.js +37 -5
- package/dist/repl/commands/registry.js +2 -0
- package/dist/repl/commands/router.js +141 -0
- package/dist/repl/commands/tool-group.js +33 -1
- package/dist/repl/commands.js +11 -1
- package/dist/repl/runtime.js +14 -15
- package/dist/session/compact.js +4 -0
- package/dist/session/gui-actions.js +164 -0
- package/dist/tools/builtins/index.js +4 -1
- package/dist/tools/jev-client.js +192 -0
- package/dist/tools/router.js +85 -2
- package/dist/ui/layout-internal/content-write.js +70 -4
- package/dist/ui/layout-internal/state.js +3 -0
- package/package.json +1 -1
package/dist/agent/model-turn.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { estimatePromptTokens, estimateTokens, isContextLengthError, } from '../llm/index.js';
|
|
2
|
+
import { visionBatch, visionKeep } from '../config/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* 沿 cause 链(≤3 层)取第一个 errno,供 trace 取证。
|
|
4
5
|
* undici 把底层 errno 挂在 cause 上(`TypeError: fetch failed` → cause `read ECONNRESET`),
|
|
@@ -33,6 +34,14 @@ export async function runModelTurn(input) {
|
|
|
33
34
|
onContextUpdate?.();
|
|
34
35
|
let historyRebuilt = false;
|
|
35
36
|
let overflowRetried = false;
|
|
37
|
+
// 视觉滑动窗口**必须剪在 trim 之前**:contextTrimmer.trim() 拿的是 historyManager.snapshot(),
|
|
38
|
+
// 若先 trim,预算口径还是未剪的旧数组,80% 压力线照旧被图像撑爆(design-notes/vision-window.md §2.1)。
|
|
39
|
+
// 只在这一个地方剪;下面的 overflow 重试路径读同一个 snapshot,会自动受益。
|
|
40
|
+
const visionKeepN = visionKeep();
|
|
41
|
+
if (visionKeepN > 0 && historyManager.pruneVisionWindow({ keep: visionKeepN, batch: visionBatch(), step })) {
|
|
42
|
+
rebuildHistoryIndexes();
|
|
43
|
+
onContextUpdate?.();
|
|
44
|
+
}
|
|
36
45
|
const compactStartedAt = Date.now();
|
|
37
46
|
const trimResult = await contextTrimmer.trim({
|
|
38
47
|
mode: scheduler ? 'scheduled' : 'fallback',
|
|
@@ -98,7 +107,7 @@ export async function runModelTurn(input) {
|
|
|
98
107
|
? '## Post-compaction recovery\n' +
|
|
99
108
|
'Context was compacted before this request. Recover before doing anything else, in this order:\n' +
|
|
100
109
|
'1. Read the session summary at the top of the history: `## Completed` is already done — do not redo or re-verify it. `## In Progress` / `## Next Steps` tell you exactly where work stopped and what is next.\n' +
|
|
101
|
-
'2. Read `## Session state` below (from notes.md
|
|
110
|
+
'2. Read `## Session state` below (refreshed every step from notes.md / gui-actions.log): the active plan is authoritative — `[x]` steps are finished, resume from the first `[ ]`. A `## Compaction Snapshot` section there is the progress checkpoint written at this compaction. A `## GUI actions` section lists every GUI action already performed with its observed result: do not repeat an action that appears there, unless the latest screenshot contradicts it (then the screenshot wins — treat that line as attempted but unverified).\n' +
|
|
102
111
|
(sessionStateText
|
|
103
112
|
? ''
|
|
104
113
|
: '(No active plan or snapshot was found in notes.md — reconstruct what is done purely from the summary and treat its `## Completed` as ground truth.)\n') +
|
|
@@ -11,7 +11,7 @@ import { defaultAgentRuntimeContext } from './runtime-context.js';
|
|
|
11
11
|
import { runModelTurn } from './model-turn.js';
|
|
12
12
|
import { runToolTurn } from './tool-turn.js';
|
|
13
13
|
import { createTurnLifecycle } from './turn-lifecycle.js';
|
|
14
|
-
import { parseArgs, argumentErrorHint, isParallelTool, isResourceLockedCall, deniedOutcome, readDiffContext, pushToolResult, } from './tool-helpers.js';
|
|
14
|
+
import { parseArgs, argumentErrorHint, isParallelTool, isParallelOrchestrationCall, isResourceLockedCall, deniedOutcome, readDiffContext, pushToolResult, } from './tool-helpers.js';
|
|
15
15
|
import { contextState, summarizeToolArguments } from '../session/index.js';
|
|
16
16
|
import { createBudgetScheduler } from '../session/scheduler.js';
|
|
17
17
|
import { invalidateArtifacts, rehydrateArtifacts } from '../context/index.js';
|
|
@@ -522,6 +522,98 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
522
522
|
hooks.onToolDone?.();
|
|
523
523
|
i = j;
|
|
524
524
|
}
|
|
525
|
+
else if (isParallelOrchestrationCall(currentCall.name, ctx.toolRuntime) &&
|
|
526
|
+
!(ctx.getAgentMode() === 'plan' && planDisabledTools.has(currentCall.name))) {
|
|
527
|
+
// 连续编排调用(sub-agent):按 subAgentConcurrency 分块,块内并发。
|
|
528
|
+
// 权限确认仍严格按原序进行;全部 header 必须先于首个 execute 发出——渲染侧
|
|
529
|
+
// 靠 header 建「组容器批」并逐条追加 └─ 子 agent 行,若边启动边发 header,
|
|
530
|
+
// 先完成的 entry 会让组提前收口,后续 header 会另起一个新组。
|
|
531
|
+
let j = i;
|
|
532
|
+
while (j < calls.length &&
|
|
533
|
+
isParallelOrchestrationCall(calls[j].name, ctx.toolRuntime) &&
|
|
534
|
+
!isToolDeniedForStep(calls[j].name) &&
|
|
535
|
+
!(ctx.getAgentMode() === 'plan' && planDisabledTools.has(calls[j].name)))
|
|
536
|
+
j++;
|
|
537
|
+
const batch = calls.slice(i, j);
|
|
538
|
+
const entries = [];
|
|
539
|
+
for (let k = 0; k < batch.length; k++) {
|
|
540
|
+
const tc = batch[k];
|
|
541
|
+
const parsed = parseArgs(tc.arguments);
|
|
542
|
+
const tool = ctx.toolRuntime.findTool(tc.name);
|
|
543
|
+
const argumentsValid = tool && parsed !== null ? validateToolArguments(tool, parsed).valid : false;
|
|
544
|
+
let denied;
|
|
545
|
+
if (tool && argumentsValid) {
|
|
546
|
+
const perm = await ctx.checkPermission(tool, parsed ?? {}, signal, {
|
|
547
|
+
prompt: opts.permissionPrompt,
|
|
548
|
+
});
|
|
549
|
+
emitTrace('permission', {
|
|
550
|
+
source: 'agent_tool',
|
|
551
|
+
tool: tc.name,
|
|
552
|
+
decision: perm,
|
|
553
|
+
argumentHash: tracedCalls[i + k].args.sha256,
|
|
554
|
+
}, {
|
|
555
|
+
toolCallId: tracedCalls[i + k].toolCallId,
|
|
556
|
+
...(tc.id ? { providerToolCallId: tc.id } : {}),
|
|
557
|
+
});
|
|
558
|
+
if (perm === 'deny')
|
|
559
|
+
denied = deniedOutcome(tc.name);
|
|
560
|
+
}
|
|
561
|
+
entries.push({
|
|
562
|
+
tc,
|
|
563
|
+
parsed,
|
|
564
|
+
diff: { preWriteOld: null, editStartLine: 1 },
|
|
565
|
+
...(denied ? { denied } : {}),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
for (const entry of entries)
|
|
569
|
+
hooks.onToolHeader?.(entry.tc);
|
|
570
|
+
const firstAllowed = entries.find((entry) => !entry.denied);
|
|
571
|
+
if (firstAllowed)
|
|
572
|
+
hooks.onToolStart?.(firstAllowed.tc.name);
|
|
573
|
+
const concurrency = Math.max(1, ctx.config.subAgentConcurrency);
|
|
574
|
+
for (let chunkStart = 0; chunkStart < entries.length; chunkStart += concurrency) {
|
|
575
|
+
const chunk = entries.slice(chunkStart, chunkStart + concurrency);
|
|
576
|
+
// 块内同时启动(executeToolOutcome 调用即开始 I/O),再按原序 await + 回灌:
|
|
577
|
+
// 完成顺序任意,history 与 trace 顺序始终是原调用序。
|
|
578
|
+
const started = chunk.map((entry) => {
|
|
579
|
+
if (entry.denied)
|
|
580
|
+
return Promise.resolve(entry.denied);
|
|
581
|
+
const hint = argumentErrorHint(entry.tc.name, runtimeContextState);
|
|
582
|
+
return ctx.toolRuntime.executeToolOutcome(entry.tc.name, entry.tc.arguments, signal, {
|
|
583
|
+
callId: entry.tc.id,
|
|
584
|
+
allowedToolNames: currentAllowedToolNames(),
|
|
585
|
+
delegation: delegationForOrchestrator(),
|
|
586
|
+
...(hint ? { argumentErrorHint: hint } : {}),
|
|
587
|
+
onLockAcquired: (lockedArgs) => {
|
|
588
|
+
entry.diff = readDiffContext(entry.tc, lockedArgs, ctx.jailResolve);
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
for (let k = 0; k < chunk.length; k++) {
|
|
593
|
+
const entry = chunk[k];
|
|
594
|
+
const outcome = await started[k];
|
|
595
|
+
usageMeter.add(outcome.usage);
|
|
596
|
+
opts.onToolOutcome?.(entry.tc.name, entry.parsed ?? {}, outcome);
|
|
597
|
+
traceToolEnd(entry.tc, i + chunkStart + k, outcome);
|
|
598
|
+
hooks.onToolResult?.(entry.tc, outcome.output, entry.denied ? null : entry.parsed, entry.diff.preWriteOld, entry.diff.editStartLine);
|
|
599
|
+
pushToolResult(history, entry.tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
600
|
+
const invalidatedFiles = [
|
|
601
|
+
...new Set([...(outcome.changedFiles ?? []), ...(outcome.staleFiles ?? [])]),
|
|
602
|
+
];
|
|
603
|
+
if (invalidatedFiles.length > 0) {
|
|
604
|
+
for (const changedFile of invalidatedFiles) {
|
|
605
|
+
relprune?.observeMutation(history, changedFile);
|
|
606
|
+
lifecycle?.pushMutation(history, history.length - 1, changedFile);
|
|
607
|
+
}
|
|
608
|
+
invalidateArtifacts(runtimeContextState, history, invalidatedFiles);
|
|
609
|
+
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (firstAllowed)
|
|
614
|
+
hooks.onToolDone?.();
|
|
615
|
+
i = j;
|
|
616
|
+
}
|
|
525
617
|
else if (isResourceLockedCall(currentCall, ctx.toolRuntime) &&
|
|
526
618
|
!(ctx.getAgentMode() === 'plan' && planDisabledTools.has(currentCall.name))) {
|
|
527
619
|
// 连续文件 mutation:权限确认仍严格按原序进行;全部 preflight 完成后再启动。
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyVisionWindow, recordVisionWindowPrune } from '../../context/vision-window.js';
|
|
1
2
|
function replaceMessages(backing, messages) {
|
|
2
3
|
const replacement = messages.slice();
|
|
3
4
|
backing.length = 0;
|
|
@@ -147,6 +148,18 @@ class DefaultHistoryManager {
|
|
|
147
148
|
replaceMessages(this.backing, result.messages);
|
|
148
149
|
this.revision++;
|
|
149
150
|
}
|
|
151
|
+
pruneVisionWindow(opts) {
|
|
152
|
+
this.assertNoActiveBatch('prune vision window');
|
|
153
|
+
const result = applyVisionWindow(this.backing, opts);
|
|
154
|
+
if (!result.changed)
|
|
155
|
+
return false;
|
|
156
|
+
replaceMessages(this.backing, result.messages);
|
|
157
|
+
// 累积埋点放在纯函数模块外部(HistoryManager 这层),单测可用 reset 隔离。
|
|
158
|
+
recordVisionWindowPrune(result.dropped);
|
|
159
|
+
// revision++ 必须:snapshot() 用 revision 标识版本,下游(compact / checkpoint)据此判断是否重建。
|
|
160
|
+
this.revision++;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
150
163
|
createCheckpoint() {
|
|
151
164
|
return { revision: this.revision, messages: this.backing.slice() };
|
|
152
165
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ADD_TOOL_GROUPS_TOOL_NAME } from '../../config/profiles.js';
|
|
2
|
+
import { config } from '../../config/index.js';
|
|
2
3
|
import { t } from '../../i18n/index.js';
|
|
3
4
|
import { checkPermission as defaultCheckPermission } from '../../permissions/index.js';
|
|
4
5
|
import { jailResolve as defaultJailResolve } from '../../sandbox/index.js';
|
|
@@ -6,7 +7,7 @@ import { summarizeToolArguments } from '../../session/index.js';
|
|
|
6
7
|
import { getPlanDisabledTools } from '../../tools/constants.js';
|
|
7
8
|
import { defaultToolRuntime } from '../../tools/registry.js';
|
|
8
9
|
import { validateToolArguments } from '../../tools/validation.js';
|
|
9
|
-
import { deniedOutcome, isParallelTool, isResourceLockedCall, parseArgs, readDiffContext } from '../tool-helpers.js';
|
|
10
|
+
import { deniedOutcome, isParallelTool, isParallelOrchestrationCall, isResourceLockedCall, parseArgs, readDiffContext, } from '../tool-helpers.js';
|
|
10
11
|
const DEFAULT_DEPENDENCIES = {
|
|
11
12
|
toolRuntime: defaultToolRuntime,
|
|
12
13
|
checkPermission: defaultCheckPermission,
|
|
@@ -205,6 +206,70 @@ class LegacyCompatibleToolDispatcher {
|
|
|
205
206
|
index = end;
|
|
206
207
|
continue;
|
|
207
208
|
}
|
|
209
|
+
if (isParallelOrchestrationCall(current.name, toolRuntime) &&
|
|
210
|
+
!(request.policy.mode === 'plan' && getPlanDisabledTools().has(current.name))) {
|
|
211
|
+
// 连续编排调用(sub-agent):按并发上限分块,块内并发。权限确认按原序;全部 header
|
|
212
|
+
// 必须先于首个 execute——渲染侧靠 header 建组容器批,先收口会让后续 header 另起新组。
|
|
213
|
+
const concurrency = Math.max(1, request.orchestrationConcurrency ?? config.subAgentConcurrency);
|
|
214
|
+
let end = index;
|
|
215
|
+
while (end < calls.length &&
|
|
216
|
+
isParallelOrchestrationCall(calls[end].name, toolRuntime) &&
|
|
217
|
+
!request.isDenied(calls[end].name) &&
|
|
218
|
+
!(request.policy.mode === 'plan' && getPlanDisabledTools().has(calls[end].name)))
|
|
219
|
+
end++;
|
|
220
|
+
const batch = calls.slice(index, end);
|
|
221
|
+
const entries = [];
|
|
222
|
+
for (let offset = 0; offset < batch.length; offset++) {
|
|
223
|
+
const call = batch[offset];
|
|
224
|
+
const parsed = parseArgs(call.arguments);
|
|
225
|
+
const tool = toolRuntime.findTool(call.name);
|
|
226
|
+
const argumentsValid = tool && parsed !== null ? validateToolArguments(tool, parsed).valid : false;
|
|
227
|
+
let denied;
|
|
228
|
+
if (tool && argumentsValid) {
|
|
229
|
+
const decision = await checkPermission(tool, parsed ?? {}, request.signal, {
|
|
230
|
+
prompt: request.permissionPrompt,
|
|
231
|
+
});
|
|
232
|
+
request.onEvent({ type: 'permission', call, callIndex: index + offset, decision });
|
|
233
|
+
if (decision === 'deny')
|
|
234
|
+
denied = deniedOutcome(call.name);
|
|
235
|
+
}
|
|
236
|
+
entries.push({
|
|
237
|
+
call,
|
|
238
|
+
parsed,
|
|
239
|
+
diff: { preWriteOld: null, editStartLine: 1 },
|
|
240
|
+
...(denied ? { denied } : {}),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
for (const entry of entries)
|
|
244
|
+
request.onEvent({ type: 'header', call: entry.call });
|
|
245
|
+
const firstAllowed = entries.find((entry) => !entry.denied);
|
|
246
|
+
if (firstAllowed)
|
|
247
|
+
request.onEvent({ type: 'start', tool: firstAllowed.call.name });
|
|
248
|
+
for (let chunkStart = 0; chunkStart < entries.length; chunkStart += concurrency) {
|
|
249
|
+
const chunk = entries.slice(chunkStart, chunkStart + concurrency);
|
|
250
|
+
// 块内同时启动,再按原序 await + 回灌:完成顺序任意,history/trace 始终是原调用序。
|
|
251
|
+
const started = chunk.map((entry) => {
|
|
252
|
+
if (entry.denied)
|
|
253
|
+
return Promise.resolve(entry.denied);
|
|
254
|
+
return execute(entry.call, request.argumentErrorHint(entry.call.name), (lockedArgs) => {
|
|
255
|
+
entry.diff = readDiffContext(entry.call, lockedArgs, jailResolve);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
for (let k = 0; k < chunk.length; k++) {
|
|
259
|
+
const callIndex = index + chunkStart + k;
|
|
260
|
+
const entry = chunk[k];
|
|
261
|
+
const outcome = await started[k];
|
|
262
|
+
record(callIndex, outcome);
|
|
263
|
+
executionEvents(callIndex, entry.parsed, outcome);
|
|
264
|
+
resultEvent(callIndex, outcome, entry.denied ? null : entry.parsed, entry.diff);
|
|
265
|
+
invalidate(outcome);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (firstAllowed)
|
|
269
|
+
request.onEvent({ type: 'done' });
|
|
270
|
+
index = end;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
208
273
|
if (isResourceLockedCall(current, toolRuntime) &&
|
|
209
274
|
!(request.policy.mode === 'plan' && getPlanDisabledTools().has(current.name))) {
|
|
210
275
|
let end = index;
|
|
@@ -15,6 +15,7 @@ import { contextState } from '../session/compact.js';
|
|
|
15
15
|
import { capToolResultForHistory } from '../session/compact.js';
|
|
16
16
|
import { recordArtifact, knownEditTargets } from '../context/index.js';
|
|
17
17
|
import { isToolResultSuccess } from '../context/utils.js';
|
|
18
|
+
import { appendGuiAction } from '../session/gui-actions.js';
|
|
18
19
|
/** 解析工具 arguments JSON;非法或空返 null(调用方据此降级到普通 preview)。 */
|
|
19
20
|
export function parseArgs(raw) {
|
|
20
21
|
try {
|
|
@@ -57,9 +58,15 @@ export function isResourceLockedTool(name, toolRuntime = defaultToolRuntime) {
|
|
|
57
58
|
}
|
|
58
59
|
export function isResourceLockedCall(call, toolRuntime = defaultToolRuntime) {
|
|
59
60
|
// sub-agent 是长时全域操作(嵌套 agent 与主 agent 同权,可写任意文件/跑任意命令),
|
|
60
|
-
// 不进 mutation
|
|
61
|
+
// 不进 mutation 并发批:那个批的语义是「同文件排队、异文件并发」。
|
|
61
62
|
return call.name !== 'sub-agent' && isResourceLockedTool(call.name, toolRuntime);
|
|
62
63
|
}
|
|
64
|
+
/** 编排类工具(如 sub-agent):同一轮内连续派发的多个调用按上限成批并行。
|
|
65
|
+
* 写冲突仍由子 agent 内层工具各自获取的资源锁保护(编排器本身不持锁)。 */
|
|
66
|
+
export function isParallelOrchestrationCall(name, toolRuntime = defaultToolRuntime) {
|
|
67
|
+
const tool = toolRuntime.findTool(name);
|
|
68
|
+
return !!tool && toolRuntime.getToolCapabilities(tool).parallelOrchestration === true;
|
|
69
|
+
}
|
|
63
70
|
/** 权限拒绝时的结构化 ToolOutcome(供调度器统一回灌,不抛错中断循环)。 */
|
|
64
71
|
export function deniedOutcome(name) {
|
|
65
72
|
return {
|
|
@@ -131,4 +138,9 @@ export function pushToolResult(history, tc, output, pruner, lifecycle, _schedule
|
|
|
131
138
|
if (lifecycle)
|
|
132
139
|
lifecycle.pushTool(history, messageIndex, succeeded);
|
|
133
140
|
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
141
|
+
// GUI 动作台账(L2):每个 computer 动作往会话目录的 gui-actions.log 追加一行。
|
|
142
|
+
// 放这里而不是工具内部,是因为工具不该持有会话目录与文件 I/O;写失败静默,
|
|
143
|
+
// 台账是可观测性,绝不能反过来改变动作结果。
|
|
144
|
+
if (tc.name === 'computer')
|
|
145
|
+
appendGuiAction(output);
|
|
134
146
|
}
|
package/dist/agent/tool-turn.js
CHANGED
|
@@ -3,6 +3,12 @@ const PLAN_NAG_THRESHOLD = 3;
|
|
|
3
3
|
const PLAN_NAG_TEXT = '[mocode] Reminder: you have an active plan in notes.md but have not updated it recently. ' +
|
|
4
4
|
'If you finished a step, call plan_update to check it off (keep at most one in_progress); ' +
|
|
5
5
|
'if the whole plan is done, let plan_update settle it to ## Done:. If the plan changed scope, update it to match reality.';
|
|
6
|
+
/**
|
|
7
|
+
* 工具附件消息的前言。单点常量:视觉滑动窗口(src/context/vision-window.ts)靠它区分
|
|
8
|
+
* 「工具回灌的屏幕帧」与「用户粘贴的图」——两边各写一份字符串会让识别规则悄悄失效。
|
|
9
|
+
* 新增附件产出点**必须**复用这一个前言。
|
|
10
|
+
*/
|
|
11
|
+
export const ATTACHMENT_PREAMBLE = 'The view_image tool loaded the following visual input: ';
|
|
6
12
|
/** Owns tool-turn history publication, transaction settlement, plan nag, attachments and checkpoint ordering. */
|
|
7
13
|
export async function runToolTurn(input) {
|
|
8
14
|
const { opts, ctx, historyManager, result, stream, step, maxSteps, planState, turnLifecycle, cancellationLifecycle, terminationPolicy, rebuildHistoryIndexes, dispatch, } = input;
|
|
@@ -53,7 +59,7 @@ export async function runToolTurn(input) {
|
|
|
53
59
|
const content = [
|
|
54
60
|
{
|
|
55
61
|
type: 'text',
|
|
56
|
-
text:
|
|
62
|
+
text: `${ATTACHMENT_PREAMBLE}${names}. Analyze the attached image content directly.`,
|
|
57
63
|
},
|
|
58
64
|
...modelAttachments.map((attachment) => ({
|
|
59
65
|
type: 'image_url',
|
package/dist/config/index.js
CHANGED
|
@@ -4,11 +4,14 @@ import path from 'node:path';
|
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
5
|
import { getCurrentSessionId } from '../session/state.js';
|
|
6
6
|
import { getNotesFilePath, extractActiveNotesSections } from '../session/notes.js';
|
|
7
|
+
import { buildGuiActionsSection } from '../session/gui-actions.js';
|
|
7
8
|
import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
|
|
8
9
|
import { buildValidationCommandsSection } from '../verification/prompt.js';
|
|
9
10
|
import { getActivePresetName, readPreset } from './presets.js';
|
|
10
11
|
import { detectLanguage, setLanguage, t } from '../i18n/index.js';
|
|
11
12
|
import { isProfileName, profileHasGroup } from './profiles.js';
|
|
13
|
+
// 端点常量归协议实现方(jev-client.ts,纯叶子无 import,不引入环);此处只引用不重定义。
|
|
14
|
+
import { DEFAULT_JEV_BASE_URL } from '../tools/jev-client.js';
|
|
12
15
|
/**
|
|
13
16
|
* 按优先级加载配置文件并回填 process.env:
|
|
14
17
|
* 候选(后者覆盖前者,优先级升序):<cwd>/.env(兼容旧用法,最低)→ ~/.mocode/config(全局)→ <cwd>/.mocode/config(项目级覆盖,最高)。
|
|
@@ -286,26 +289,32 @@ export function reinjectSessionStateIntoSystem(history) {
|
|
|
286
289
|
return planChanged || notesChanged;
|
|
287
290
|
}
|
|
288
291
|
/**
|
|
289
|
-
* 构造"会话状态提醒"正文(活跃 `## Plan:` 段 +
|
|
290
|
-
*
|
|
292
|
+
* 构造"会话状态提醒"正文(活跃 `## Plan:` 段 + 活跃笔记段 + GUI 动作台账),供 agent/core
|
|
293
|
+
* 每步拼进 requestHistory **末尾**的 ephemeral system 消息。
|
|
291
294
|
*
|
|
292
295
|
* 为什么在尾部而不是 history[0](prompt 缓存):plan_update / note_append 是设计上鼓励
|
|
293
296
|
* 高频调用的工具,一旦它们改写系统提示,支持自动前缀缓存的后端(OpenAI / DeepSeek /
|
|
294
297
|
* GLM / Qwen)就会从第一个 token 起全部 miss。放到历史末尾后,前面整段(系统提示 + 全部
|
|
295
|
-
* 已有对话)
|
|
298
|
+
* 已有对话)保持逐字节稳定,只有尾部这一小条随来源文件变化。
|
|
296
299
|
*
|
|
297
|
-
*
|
|
300
|
+
* 台账段同样在尾部 → 抖动免费(anthropic provider 明确"动态 session reminder 不参与缓存
|
|
301
|
+
* 断点"),所以它可以每步都重写一遍,不需要像图片窗口那样成批淘汰。
|
|
302
|
+
*
|
|
303
|
+
* 纯读函数:不改 history,也不写文件。三者皆空时返回 ''(零开销)。
|
|
298
304
|
*/
|
|
299
305
|
export function buildSessionStateReminder(sessionId = getCurrentSessionId()) {
|
|
300
306
|
const plan = extractActivePlanSection(sessionId);
|
|
301
307
|
const notes = extractActiveNotesSections(undefined, sessionId);
|
|
302
|
-
|
|
308
|
+
const guiActions = buildGuiActionsSection(sessionId);
|
|
309
|
+
if (!plan && !notes && !guiActions)
|
|
303
310
|
return '';
|
|
311
|
+
const sources = [notes || plan ? 'notes.md' : '', guiActions ? 'gui-actions.log' : ''].filter(Boolean).join(' + ');
|
|
304
312
|
const parts = [
|
|
305
|
-
|
|
306
|
-
'This block mirrors the live session
|
|
313
|
+
`## Session state (current, from ${sources})`,
|
|
314
|
+
'This block mirrors the live session state and is refreshed every step; treat it as authoritative, and ignore any older copy earlier in this conversation.',
|
|
307
315
|
...(plan ? [plan] : []),
|
|
308
316
|
...(notes ? [notes] : []),
|
|
317
|
+
...(guiActions ? [guiActions] : []),
|
|
309
318
|
];
|
|
310
319
|
return parts.join('\n\n');
|
|
311
320
|
}
|
|
@@ -533,6 +542,7 @@ export const config = {
|
|
|
533
542
|
maxSteps: Number(process.env.MAX_STEPS) || 1000,
|
|
534
543
|
subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
|
|
535
544
|
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || Number(process.env.MAX_STEPS) || 1000,
|
|
545
|
+
subAgentConcurrency: Math.max(1, Number(process.env.SUB_AGENT_CONCURRENCY) || 5),
|
|
536
546
|
frontendToolsEnabled: process.env.MOCODE_FRONTEND_TOOLS_ENABLED === 'true',
|
|
537
547
|
computerUseEnabled: process.env.MOCODE_COMPUTER_USE_ENABLED === 'true',
|
|
538
548
|
mcpEnabled: process.env.MOCODE_MCP_ENABLED !== 'false',
|
|
@@ -681,6 +691,51 @@ export function updateComputerUseConfig(enabled) {
|
|
|
681
691
|
config.computerUseEnabled = enabled;
|
|
682
692
|
process.env.MOCODE_COMPUTER_USE_ENABLED = enabled ? 'true' : 'false';
|
|
683
693
|
}
|
|
694
|
+
// ── 视觉历史滑动窗口(Computer Use)───────────────────────────────────────
|
|
695
|
+
//
|
|
696
|
+
// 屏幕帧永驻 history 会让图像 token 二次增长(每次请求都要重发整个 history):
|
|
697
|
+
// 20 步 ≈387k tk、50 步 ≈2.35M tk,长 GUI 任务必然中途 compact 并丢掉视觉 grounding。
|
|
698
|
+
// 滑动窗口按「最近 keep 条 + 成批淘汰 batch 条」把旧帧换成文本占位。详见
|
|
699
|
+
// design-notes/vision-window.md。
|
|
700
|
+
//
|
|
701
|
+
// 这里是 process.env 直读而非 Config 单例:与其它 CU 调优项(MOCODE_CU_MAX_EDGE 等)同款,
|
|
702
|
+
// 每步都在调用点求值,改 .env 后立即生效,不需要重启 REPL。
|
|
703
|
+
/** 窗口保留的屏幕帧条数默认值。 */
|
|
704
|
+
export const DEFAULT_VISION_KEEP = 6;
|
|
705
|
+
/** 单次淘汰条数默认值。1 = 严格窗口(token 最省 / 前缀最不稳)。 */
|
|
706
|
+
export const DEFAULT_VISION_BATCH = 4;
|
|
707
|
+
/**
|
|
708
|
+
* 保留最近多少条屏幕帧。**0 = 完全关闭窗口**(回退到现状,一键回滚)。
|
|
709
|
+
*
|
|
710
|
+
* 空串语义与 `MOCODE_CU_DIFF_THRESHOLD`(src/tools/builtins/computer.ts:49)一致:
|
|
711
|
+
* **未配置 / 空串 → 回落默认**;显式写 0 才是 0。`Number('') === 0`,直接 Number 判数值
|
|
712
|
+
* 会让"注释掉这个变量"静默变成关闭窗口。
|
|
713
|
+
*/
|
|
714
|
+
export function visionKeep() {
|
|
715
|
+
const raw = process.env.MOCODE_CU_VISION_KEEP;
|
|
716
|
+
if (raw === undefined || raw.trim() === '')
|
|
717
|
+
return DEFAULT_VISION_KEEP;
|
|
718
|
+
const parsed = Number(raw);
|
|
719
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
720
|
+
return DEFAULT_VISION_KEEP;
|
|
721
|
+
return Math.round(parsed);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* 一次淘汰多少条。>=1;非法值回落默认。
|
|
725
|
+
*
|
|
726
|
+
* batch 是 **token 与 prompt cache 的权衡**:batch 越大,"两次淘汰之间 history 前缀字节不变"
|
|
727
|
+
* 的窗口越长,前缀命中率越高,代价是在途多留几张图。判据见文档 §7.2:
|
|
728
|
+
* 显式 prompt cache(Anthropic 断点)取 6-8;隐式前缀缓存取 4;无缓存取 1。
|
|
729
|
+
*/
|
|
730
|
+
export function visionBatch() {
|
|
731
|
+
const raw = process.env.MOCODE_CU_VISION_BATCH;
|
|
732
|
+
if (raw === undefined || raw.trim() === '')
|
|
733
|
+
return DEFAULT_VISION_BATCH;
|
|
734
|
+
const parsed = Number(raw);
|
|
735
|
+
if (!Number.isFinite(parsed) || parsed < 1)
|
|
736
|
+
return DEFAULT_VISION_BATCH;
|
|
737
|
+
return Math.round(parsed);
|
|
738
|
+
}
|
|
684
739
|
/** MCP 总开关;关闭时下次启动跳过 MCP 配置读取与服务连接。 */
|
|
685
740
|
export function isMcpEnabled() {
|
|
686
741
|
return config.mcpEnabled;
|
|
@@ -740,3 +795,48 @@ export function updateLanguageConfig(language) {
|
|
|
740
795
|
setLanguage(language);
|
|
741
796
|
process.env.MOCODE_LANGUAGE = language;
|
|
742
797
|
}
|
|
798
|
+
/** 当前路由模式;未知值一律回退 llm(保守:保证有可用的路由后端)。 */
|
|
799
|
+
export function getRouterMode() {
|
|
800
|
+
return process.env.MOCODE_ROUTER_MODE === 'jev' ? 'jev' : 'llm';
|
|
801
|
+
}
|
|
802
|
+
/** /router 的写入口;持久化由调用方写 MOCODE_ROUTER_MODE(见 config/file.ts)。 */
|
|
803
|
+
export function updateRouterMode(mode) {
|
|
804
|
+
process.env.MOCODE_ROUTER_MODE = mode;
|
|
805
|
+
}
|
|
806
|
+
function readNumberEnv(name, fallback) {
|
|
807
|
+
const raw = process.env[name];
|
|
808
|
+
if (raw === undefined || raw.trim() === '')
|
|
809
|
+
return fallback;
|
|
810
|
+
const value = Number(raw);
|
|
811
|
+
return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : fallback;
|
|
812
|
+
}
|
|
813
|
+
/** 读取 Jev 路由配置(每次调用实时读 env,支持 /router 即时改)。 */
|
|
814
|
+
export function getJevRouterConfig() {
|
|
815
|
+
return {
|
|
816
|
+
baseUrl: (process.env.MOCODE_ROUTER_JEV_BASE_URL || DEFAULT_JEV_BASE_URL).replace(/\/+$/, ''),
|
|
817
|
+
apiKey: process.env.MOCODE_ROUTER_JEV_API_KEY || '',
|
|
818
|
+
model: process.env.MOCODE_ROUTER_JEV_MODEL || 'jev-latest',
|
|
819
|
+
confidenceMin: readNumberEnv('MOCODE_ROUTER_CONFIDENCE_MIN', 0.65),
|
|
820
|
+
confidenceMinMcp: readNumberEnv('MOCODE_ROUTER_CONFIDENCE_MIN_MCP', 0.85),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* /router 的写入口:把 patch 中的字段写进对应环境变量。
|
|
825
|
+
* 只改内存/进程环境;持久化由调用方写 ~/.mocode/config(见 config/file.ts)。
|
|
826
|
+
*/
|
|
827
|
+
export function updateJevRouterConfig(patch) {
|
|
828
|
+
if (patch.baseUrl !== undefined)
|
|
829
|
+
process.env.MOCODE_ROUTER_JEV_BASE_URL = patch.baseUrl;
|
|
830
|
+
if (patch.apiKey !== undefined)
|
|
831
|
+
process.env.MOCODE_ROUTER_JEV_API_KEY = patch.apiKey;
|
|
832
|
+
if (patch.model !== undefined)
|
|
833
|
+
process.env.MOCODE_ROUTER_JEV_MODEL = patch.model;
|
|
834
|
+
if (patch.confidenceMin !== undefined)
|
|
835
|
+
process.env.MOCODE_ROUTER_CONFIDENCE_MIN = String(patch.confidenceMin);
|
|
836
|
+
if (patch.confidenceMinMcp !== undefined)
|
|
837
|
+
process.env.MOCODE_ROUTER_CONFIDENCE_MIN_MCP = String(patch.confidenceMinMcp);
|
|
838
|
+
}
|
|
839
|
+
/** Jev 后端是否已具备最小可用配置(有 key 才可能成功)。 */
|
|
840
|
+
export function isJevRouterConfigured() {
|
|
841
|
+
return getJevRouterConfig().apiKey.trim() !== '';
|
|
842
|
+
}
|