mocode-ai 1.6.3 → 1.6.4
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/agent/run-coordinator.js +124 -84
- package/dist/agent/stages/tool-dispatcher.js +116 -79
- package/dist/config/index.js +9 -19
- package/dist/context/encoders/passthrough.js +2 -2
- package/dist/context/pipeline.js +4 -3
- package/dist/context/relevance.js +52 -13
- package/dist/tools/builtins/edit-file.js +2 -4
- package/dist/tools/builtins/grep.js +2 -4
- package/dist/tools/builtins/read-file.js +2 -4
- package/dist/tools/policy.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -201,8 +201,8 @@ Common backend `base_url` values:
|
|
|
201
201
|
| `ANYSEARCH_API_KEY` | Web search API key (falls back to anonymous free quota if unset) | none |
|
|
202
202
|
| `ANYSEARCH_BASE_URL` | Search API endpoint | `https://api.anysearch.com` |
|
|
203
203
|
| `SKILLS_DIRS` | Override the default skill scan directories (platform path separator) | three default directories |
|
|
204
|
-
| `MOCODE_CONTEXT_OPTIMIZE` |
|
|
205
|
-
| `MOCODE_CONTEXT_RELPRUNE` |
|
|
204
|
+
| `MOCODE_CONTEXT_OPTIMIZE` | Typed encoding of Cold logs/searches, only under real pressure (set `false` to disable) | `true` |
|
|
205
|
+
| `MOCODE_CONTEXT_RELPRUNE` | Exact superseded-evidence pruning, only under real pressure (set `false` to disable) | `true` |
|
|
206
206
|
| `MOCODE_LIFECYCLE` | Provenance metadata tracking; never ages or rewrites content | `true` |
|
|
207
207
|
| `MAX_STEPS` | Max agent loop steps per turn (infinite-loop safety only) | `1000` |
|
|
208
208
|
| `SUB_AGENT_MAX_STEPS` | Sub-agent loop safety ceiling; defaults to the main-agent value | `1000` |
|
package/README.zh-CN.md
CHANGED
|
@@ -188,8 +188,8 @@ LLM_MODEL=glm-4.6 # 换成你的模型名
|
|
|
188
188
|
| `ANYSEARCH_API_KEY` | 联网搜索 API key(不配走匿名免费额度) | 无 |
|
|
189
189
|
| `ANYSEARCH_BASE_URL` | 搜索 API 端点 | `https://api.anysearch.com` |
|
|
190
190
|
| `SKILLS_DIRS` | 覆盖默认 skill 扫描目录(平台分隔符) | 三目录自动扫描 |
|
|
191
|
-
| `MOCODE_CONTEXT_OPTIMIZE` | 仅在真实 pressure 下编码 Cold
|
|
192
|
-
| `MOCODE_CONTEXT_RELPRUNE` | 仅在真实 pressure 下裁剪精确 superseded
|
|
191
|
+
| `MOCODE_CONTEXT_OPTIMIZE` | 仅在真实 pressure 下编码 Cold 日志/搜索(设 `false` 关闭) | `true` |
|
|
192
|
+
| `MOCODE_CONTEXT_RELPRUNE` | 仅在真实 pressure 下裁剪精确 superseded 证据(设 `false` 关闭) | `true` |
|
|
193
193
|
| `MOCODE_LIFECYCLE` | 只维护 provenance 元数据,不按次数改写正文 | `true` |
|
|
194
194
|
| `MAX_STEPS` | 每轮 Agent 循环最大步数(仅防无限循环) | `1000` |
|
|
195
195
|
| `SUB_AGENT_MAX_STEPS` | 子 Agent 循环安全上限,默认与主 Agent 一致 | `1000` |
|
|
@@ -367,100 +367,140 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
367
367
|
...(tc.id ? { providerToolCallId: tc.id } : {}),
|
|
368
368
|
});
|
|
369
369
|
};
|
|
370
|
-
const
|
|
370
|
+
const controlIndexes = [];
|
|
371
|
+
const otherIndexes = [];
|
|
372
|
+
calls.forEach((tc, index) => (tc.name === ADD_TOOL_GROUPS_TOOL_NAME ? controlIndexes : otherIndexes).push(index));
|
|
373
|
+
const hasToolRouteBarrier = controlIndexes.length > 0;
|
|
371
374
|
if (hasToolRouteBarrier) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
375
|
+
// 同批非控制调用全部为「未被禁用的并行安全(只读)工具」时(纯 solo 空集也满足),
|
|
376
|
+
// 先并发执行只读、再应用扩容;混有写/执行等非并行工具则整批保守拒绝。
|
|
377
|
+
const safeReadonlyBatch = otherIndexes.every((index) => isParallelTool(calls[index].name, ctx.toolRuntime) && !isToolDeniedForStep(calls[index].name));
|
|
378
|
+
if (safeReadonlyBatch) {
|
|
379
|
+
for (let index = 0; index < calls.length; index++) {
|
|
380
|
+
hooks.onToolHeader?.(calls[index]);
|
|
381
|
+
}
|
|
382
|
+
if (otherIndexes.length > 0) {
|
|
383
|
+
hooks.onToolStart?.(calls[otherIndexes[0]].name);
|
|
384
|
+
const startedReadonly = otherIndexes.map((index) => ctx.toolRuntime.executeToolOutcome(calls[index].name, calls[index].arguments, signal, {
|
|
385
|
+
callId: calls[index].id,
|
|
386
|
+
allowedToolNames: currentAllowedToolNames(),
|
|
387
|
+
delegation: delegationForOrchestrator(),
|
|
388
|
+
}));
|
|
389
|
+
for (let k = 0; k < otherIndexes.length; k++) {
|
|
390
|
+
const index = otherIndexes[k];
|
|
391
|
+
const tc = calls[index];
|
|
392
|
+
const outcome = await startedReadonly[k];
|
|
393
|
+
usageMeter.add(outcome.usage);
|
|
394
|
+
opts.onToolOutcome?.(tc.name, parseArgs(tc.arguments) ?? {}, outcome);
|
|
395
|
+
traceToolEnd(tc, index, outcome);
|
|
396
|
+
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
397
|
+
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
398
|
+
}
|
|
399
|
+
hooks.onToolDone?.();
|
|
400
|
+
}
|
|
401
|
+
for (const index of controlIndexes) {
|
|
402
|
+
const tc = calls[index];
|
|
403
|
+
const parsed = parseArgs(tc.arguments);
|
|
404
|
+
let outcome;
|
|
405
|
+
if (isToolDeniedForStep(tc.name)) {
|
|
406
|
+
outcome = {
|
|
407
|
+
status: 'denied',
|
|
408
|
+
code: 'TOOL_DISABLED',
|
|
409
|
+
retryable: false,
|
|
410
|
+
output: `错误:当前 tool policy snapshot 不允许调用 ${tc.name}。`,
|
|
411
|
+
changedFiles: [],
|
|
412
|
+
durationMs: 0,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
else if (!opts.toolPolicy) {
|
|
416
|
+
outcome = {
|
|
417
|
+
status: 'denied',
|
|
418
|
+
code: 'TOOL_DISABLED',
|
|
419
|
+
retryable: false,
|
|
420
|
+
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
421
|
+
changedFiles: [],
|
|
422
|
+
durationMs: 0,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
else if (!parsed ||
|
|
426
|
+
!Array.isArray(parsed.groups) ||
|
|
427
|
+
parsed.groups.length === 0 ||
|
|
428
|
+
typeof parsed.reason !== 'string' ||
|
|
429
|
+
!parsed.reason.trim()) {
|
|
430
|
+
outcome = {
|
|
431
|
+
status: 'error',
|
|
432
|
+
code: 'INVALID_ARGUMENTS',
|
|
433
|
+
retryable: false,
|
|
434
|
+
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
435
|
+
changedFiles: [],
|
|
436
|
+
durationMs: 0,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
const expansion = opts.toolPolicy.expand(parsed.groups, parsed.reason);
|
|
441
|
+
const succeeded = expansion.added.length > 0;
|
|
442
|
+
const details = [
|
|
443
|
+
succeeded
|
|
444
|
+
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
445
|
+
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
446
|
+
expansion.implied.length > 0
|
|
447
|
+
? `Implied groups also activated: ${expansion.implied.join(', ')}.`
|
|
448
|
+
: '',
|
|
449
|
+
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
450
|
+
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
451
|
+
]
|
|
452
|
+
.filter(Boolean)
|
|
453
|
+
.join('\n');
|
|
454
|
+
outcome = {
|
|
455
|
+
status: succeeded ? 'success' : 'error',
|
|
456
|
+
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
457
|
+
retryable: false,
|
|
458
|
+
output: details,
|
|
459
|
+
changedFiles: [],
|
|
460
|
+
durationMs: 0,
|
|
461
|
+
};
|
|
462
|
+
emitTrace('tool_route_expand', {
|
|
463
|
+
policyId: expansion.snapshot.id,
|
|
464
|
+
fromVersion: policySnapshot?.version,
|
|
465
|
+
toVersion: expansion.snapshot.version,
|
|
466
|
+
requestedGroups: parsed.groups.map(String),
|
|
467
|
+
addedGroups: expansion.added,
|
|
468
|
+
impliedGroups: expansion.implied,
|
|
469
|
+
rejected: expansion.rejected,
|
|
470
|
+
reason: parsed.reason,
|
|
471
|
+
status: outcome.status,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
opts.onToolOutcome?.(tc.name, parsed ?? {}, outcome);
|
|
475
|
+
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
476
|
+
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
477
|
+
traceToolEnd(tc, index, outcome);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// 不安全批:不扩容、不执行普通工具,逐 call 按原序配对拒绝结果。
|
|
482
|
+
for (let index = 0; index < calls.length; index++) {
|
|
483
|
+
const tc = calls[index];
|
|
484
|
+
hooks.onToolHeader?.(tc);
|
|
379
485
|
const isControl = tc.name === ADD_TOOL_GROUPS_TOOL_NAME;
|
|
380
|
-
|
|
486
|
+
const isReadonly = isParallelTool(tc.name, ctx.toolRuntime);
|
|
487
|
+
const outcome = {
|
|
381
488
|
status: 'denied',
|
|
382
489
|
code: isControl ? 'INVALID_ARGUMENTS' : 'TOOL_DISABLED',
|
|
383
490
|
retryable: false,
|
|
384
491
|
output: isControl
|
|
385
|
-
? '错误:add_tool_groups
|
|
386
|
-
:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
else if (isToolDeniedForStep(tc.name)) {
|
|
392
|
-
outcome = {
|
|
393
|
-
status: 'denied',
|
|
394
|
-
code: 'TOOL_DISABLED',
|
|
395
|
-
retryable: false,
|
|
396
|
-
output: `错误:当前 tool policy snapshot 不允许调用 ${tc.name}。`,
|
|
492
|
+
? '错误:add_tool_groups 只能单独调用,或与只读工具(read_file/glob/grep/web 等)同批;本次没有扩容。'
|
|
493
|
+
: isReadonly
|
|
494
|
+
? `错误:同一响应包含 add_tool_groups,工具 ${tc.name} 未执行。请在下一 step 重试。`
|
|
495
|
+
: `错误:add_tool_groups 不能与写/执行工具 ${tc.name} 同批;请先完成扩容,再在下一 step 调用 ${tc.name}。`,
|
|
397
496
|
changedFiles: [],
|
|
398
497
|
durationMs: 0,
|
|
399
498
|
};
|
|
499
|
+
opts.onToolOutcome?.(tc.name, parseArgs(tc.arguments) ?? {}, outcome);
|
|
500
|
+
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
501
|
+
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, false);
|
|
502
|
+
traceToolEnd(tc, index, outcome);
|
|
400
503
|
}
|
|
401
|
-
else if (!opts.toolPolicy) {
|
|
402
|
-
outcome = {
|
|
403
|
-
status: 'denied',
|
|
404
|
-
code: 'TOOL_DISABLED',
|
|
405
|
-
retryable: false,
|
|
406
|
-
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
407
|
-
changedFiles: [],
|
|
408
|
-
durationMs: 0,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
else if (!parsed ||
|
|
412
|
-
!Array.isArray(parsed.groups) ||
|
|
413
|
-
parsed.groups.length === 0 ||
|
|
414
|
-
typeof parsed.reason !== 'string' ||
|
|
415
|
-
!parsed.reason.trim()) {
|
|
416
|
-
outcome = {
|
|
417
|
-
status: 'error',
|
|
418
|
-
code: 'INVALID_ARGUMENTS',
|
|
419
|
-
retryable: false,
|
|
420
|
-
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
421
|
-
changedFiles: [],
|
|
422
|
-
durationMs: 0,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
const expansion = opts.toolPolicy.expand(parsed.groups, parsed.reason);
|
|
427
|
-
const succeeded = expansion.added.length > 0;
|
|
428
|
-
const details = [
|
|
429
|
-
succeeded
|
|
430
|
-
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
431
|
-
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
432
|
-
expansion.implied.length > 0
|
|
433
|
-
? `Implied groups also activated: ${expansion.implied.join(', ')}.`
|
|
434
|
-
: '',
|
|
435
|
-
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
436
|
-
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
437
|
-
]
|
|
438
|
-
.filter(Boolean)
|
|
439
|
-
.join('\n');
|
|
440
|
-
outcome = {
|
|
441
|
-
status: succeeded ? 'success' : 'error',
|
|
442
|
-
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
443
|
-
retryable: false,
|
|
444
|
-
output: details,
|
|
445
|
-
changedFiles: [],
|
|
446
|
-
durationMs: 0,
|
|
447
|
-
};
|
|
448
|
-
emitTrace('tool_route_expand', {
|
|
449
|
-
policyId: expansion.snapshot.id,
|
|
450
|
-
fromVersion: policySnapshot?.version,
|
|
451
|
-
toVersion: expansion.snapshot.version,
|
|
452
|
-
requestedGroups: parsed.groups.map(String),
|
|
453
|
-
addedGroups: expansion.added,
|
|
454
|
-
impliedGroups: expansion.implied,
|
|
455
|
-
rejected: expansion.rejected,
|
|
456
|
-
reason: parsed.reason,
|
|
457
|
-
status: outcome.status,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
opts.onToolOutcome?.(tc.name, parsed ?? {}, outcome);
|
|
461
|
-
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
462
|
-
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
463
|
-
traceToolEnd(tc, index, outcome);
|
|
464
504
|
}
|
|
465
505
|
}
|
|
466
506
|
// add_tool_groups 是 step 屏障:只要本响应出现该控制调用,本批所有普通工具都不执行。
|
|
@@ -76,95 +76,132 @@ class LegacyCompatibleToolDispatcher {
|
|
|
76
76
|
argumentSummary: argumentSummaries[index],
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
-
const
|
|
79
|
+
const controlIndexes = [];
|
|
80
|
+
const otherIndexes = [];
|
|
81
|
+
calls.forEach((call, index) => (call.name === ADD_TOOL_GROUPS_TOOL_NAME ? controlIndexes : otherIndexes).push(index));
|
|
82
|
+
const hasRouteBarrier = controlIndexes.length > 0;
|
|
80
83
|
if (hasRouteBarrier) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
// 同批的非控制调用全部是「未被禁用的并行安全(只读)工具」时(纯 solo 时空集也满足),
|
|
85
|
+
// 允许它们与扩容同批:先并发执行只读调用,再应用扩容。只读结果本 step 即得,新增 schema
|
|
86
|
+
// 下一 step 生效——不必为扩容空耗一轮。只要混有写/执行等非并行工具(或某只读调用已被
|
|
87
|
+
// 当前 snapshot 拒绝),即退回整批保守拒绝。
|
|
88
|
+
const safeReadonlyBatch = otherIndexes.every((index) => isParallelTool(calls[index].name, toolRuntime) && !request.isDenied(calls[index].name));
|
|
89
|
+
if (safeReadonlyBatch) {
|
|
90
|
+
// 所有 header 按调用原序先发(渲染侧据此建组容器),只读批的 header 必须先于 execute。
|
|
91
|
+
for (let index = 0; index < calls.length; index++) {
|
|
92
|
+
request.onEvent({ type: 'header', call: calls[index] });
|
|
93
|
+
}
|
|
94
|
+
if (otherIndexes.length > 0) {
|
|
95
|
+
request.onEvent({ type: 'start', tool: calls[otherIndexes[0]].name });
|
|
96
|
+
const startedReadonly = otherIndexes.map((index) => execute(calls[index]));
|
|
97
|
+
for (let offset = 0; offset < otherIndexes.length; offset++) {
|
|
98
|
+
const index = otherIndexes[offset];
|
|
99
|
+
const outcome = await startedReadonly[offset];
|
|
100
|
+
record(index, outcome);
|
|
101
|
+
executionEvents(index, parseArgs(calls[index].arguments), outcome);
|
|
102
|
+
resultEvent(index, outcome, null);
|
|
103
|
+
}
|
|
104
|
+
request.onEvent({ type: 'done' });
|
|
105
|
+
}
|
|
106
|
+
// 控制调用(header 已发):逐个校验并应用扩容;solo 与同批语义一致。
|
|
107
|
+
for (const index of controlIndexes) {
|
|
108
|
+
const call = calls[index];
|
|
109
|
+
const parsed = parseArgs(call.arguments);
|
|
110
|
+
let outcome;
|
|
111
|
+
if (request.isDenied(call.name)) {
|
|
112
|
+
outcome = {
|
|
113
|
+
status: 'denied',
|
|
114
|
+
code: 'TOOL_DISABLED',
|
|
115
|
+
retryable: false,
|
|
116
|
+
output: `错误:当前 tool policy snapshot 不允许调用 ${call.name}。`,
|
|
117
|
+
changedFiles: [],
|
|
118
|
+
durationMs: 0,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
else if (!request.expandToolGroups) {
|
|
122
|
+
outcome = {
|
|
123
|
+
status: 'denied',
|
|
124
|
+
code: 'TOOL_DISABLED',
|
|
125
|
+
retryable: false,
|
|
126
|
+
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
127
|
+
changedFiles: [],
|
|
128
|
+
durationMs: 0,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
else if (!parsed ||
|
|
132
|
+
!Array.isArray(parsed.groups) ||
|
|
133
|
+
parsed.groups.length === 0 ||
|
|
134
|
+
typeof parsed.reason !== 'string' ||
|
|
135
|
+
!parsed.reason.trim()) {
|
|
136
|
+
outcome = {
|
|
137
|
+
status: 'error',
|
|
138
|
+
code: 'INVALID_ARGUMENTS',
|
|
139
|
+
retryable: false,
|
|
140
|
+
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
141
|
+
changedFiles: [],
|
|
142
|
+
durationMs: 0,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const expansion = request.expandToolGroups(parsed.groups, parsed.reason);
|
|
147
|
+
const succeeded = expansion.added.length > 0;
|
|
148
|
+
const details = [
|
|
149
|
+
succeeded
|
|
150
|
+
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
151
|
+
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
152
|
+
expansion.implied.length > 0 ? `Implied groups also activated: ${expansion.implied.join(', ')}.` : '',
|
|
153
|
+
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
154
|
+
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
155
|
+
]
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join('\n');
|
|
158
|
+
outcome = {
|
|
159
|
+
status: succeeded ? 'success' : 'error',
|
|
160
|
+
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
161
|
+
retryable: false,
|
|
162
|
+
output: details,
|
|
163
|
+
changedFiles: [],
|
|
164
|
+
durationMs: 0,
|
|
165
|
+
};
|
|
166
|
+
request.onEvent({
|
|
167
|
+
type: 'route_expand',
|
|
168
|
+
fromVersion: request.policy.toolPolicy?.version,
|
|
169
|
+
expansion,
|
|
170
|
+
requestedGroups: parsed.groups,
|
|
171
|
+
reason: parsed.reason,
|
|
172
|
+
status: outcome.status,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
record(index, outcome);
|
|
176
|
+
request.onEvent({ type: 'host_outcome', call, parsed: parsed ?? {}, outcome });
|
|
177
|
+
resultEvent(index, outcome, null);
|
|
178
|
+
traceEnd(index, outcome);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// 不安全批(混有写/执行等非并行工具):不扩容、不执行任何普通工具,逐 call 按原序配对拒绝结果。
|
|
183
|
+
for (let index = 0; index < calls.length; index++) {
|
|
184
|
+
const call = calls[index];
|
|
185
|
+
request.onEvent({ type: 'header', call });
|
|
88
186
|
const isControl = call.name === ADD_TOOL_GROUPS_TOOL_NAME;
|
|
89
|
-
|
|
187
|
+
const isReadonly = isParallelTool(call.name, toolRuntime);
|
|
188
|
+
const outcome = {
|
|
90
189
|
status: 'denied',
|
|
91
190
|
code: isControl ? 'INVALID_ARGUMENTS' : 'TOOL_DISABLED',
|
|
92
191
|
retryable: false,
|
|
93
192
|
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}。`,
|
|
193
|
+
? '错误:add_tool_groups 只能单独调用,或与只读工具(read_file/glob/grep/web 等)同批;本次没有扩容。'
|
|
194
|
+
: isReadonly
|
|
195
|
+
? `错误:同一响应包含 add_tool_groups,工具 ${call.name} 未执行。请在下一 step 重试。`
|
|
196
|
+
: `错误:add_tool_groups 不能与写/执行工具 ${call.name} 同批;请先完成扩容,再在下一 step 调用 ${call.name}。`,
|
|
106
197
|
changedFiles: [],
|
|
107
198
|
durationMs: 0,
|
|
108
199
|
};
|
|
200
|
+
record(index, outcome);
|
|
201
|
+
request.onEvent({ type: 'host_outcome', call, parsed: parseArgs(call.arguments) ?? {}, outcome });
|
|
202
|
+
resultEvent(index, outcome, null);
|
|
203
|
+
traceEnd(index, outcome);
|
|
109
204
|
}
|
|
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
205
|
}
|
|
169
206
|
}
|
|
170
207
|
let index = hasRouteBarrier ? calls.length : 0;
|
package/dist/config/index.js
CHANGED
|
@@ -497,24 +497,14 @@ ${buildVoiceSection()}
|
|
|
497
497
|
// ToolPolicyController.reminder() 按当前 turn 的 route 注入,避免旧全局 profile 与真实 schema 分裂。
|
|
498
498
|
// 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
|
|
499
499
|
const dynamicParts = [];
|
|
500
|
-
// 会话级私有尾段(子 agent 切片会丢弃)
|
|
500
|
+
// 会话级私有尾段(子 agent 切片会丢弃)。只放一句指针:plan/note 的完整规则由
|
|
501
|
+
// plan_update / note_append 的 description 承载(两工具在常驻面,调用时一定可见),
|
|
502
|
+
// 避免把规则在系统提示里每请求预付第三遍。**标题 '## Session state' 必须保留**:
|
|
503
|
+
// buildMocodeCorePrompt 靠 MARKER_DROPPABLE_SECTION 给子 agent 切片。
|
|
501
504
|
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.');
|
|
505
|
+
'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`. ' +
|
|
506
|
+
'Both write notes.md, which survives compaction — run read_file on it to recover context after compaction. ' +
|
|
507
|
+
'Their formats and rules live in each tool\u2019s description; do not hand-edit the files.');
|
|
518
508
|
// 日期段:模型需要知道今天才能判断 freshness(web 搜索、版本时效)。只随天变化,
|
|
519
509
|
// 不破坏会话内前缀缓存;置于 '## Session state' 之前,子 agent 切片仍保留(无害且有用)。
|
|
520
510
|
const todaySection = buildTodaySection();
|
|
@@ -592,8 +582,8 @@ export const config = {
|
|
|
592
582
|
? process.env.ANTHROPIC_PROMPT_CACHE !== 'false'
|
|
593
583
|
: (__activePreset?.anthropicPromptCache ?? process.env.ANTHROPIC_PROMPT_CACHE !== 'false'),
|
|
594
584
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
595
|
-
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE
|
|
596
|
-
contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE
|
|
585
|
+
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
|
|
586
|
+
contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE !== 'false',
|
|
597
587
|
contextLifecycle: process.env.MOCODE_LIFECYCLE !== 'false',
|
|
598
588
|
contextBudget: process.env.MOCODE_BUDGET_SCHEDULER !== 'false',
|
|
599
589
|
autoReflect: process.env.AUTO_REFLECT === 'true',
|
|
@@ -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
|
}
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
import { canonicalizePath, extractPath, isToolResultSuccess, toText } from './utils.js';
|
|
4
4
|
/** Shared prefix lets /context count read and observation supersession together. */
|
|
5
5
|
const STUB_PREFIX = '⌦[已过时:';
|
|
6
|
-
|
|
6
|
+
/** read→read:仅同 path 同区间(offset+limit)的更新 read 才淘汰旧页。 */
|
|
7
|
+
const READ_RANGE_STUB_REASON = '同 path 同区间已有新 read';
|
|
8
|
+
/** mutation→read:文件被改写,该 path 旧 read 全区间淘汰。 */
|
|
9
|
+
const READ_MUTATION_STUB_REASON = '同 path 已被 mutation 覆写';
|
|
10
|
+
/** read_file 默认分页(对齐 tools/builtins/read-file.ts 的 DEFAULT_READ_LIMIT)。 */
|
|
11
|
+
const READ_DEFAULT_LIMIT = 300;
|
|
12
|
+
/** read_file 单次硬上限(对齐 MAX_FILE_LINES),把超传 limit 归一化到真实拿到的区间。 */
|
|
13
|
+
const READ_LIMIT_CAP = 2000;
|
|
7
14
|
function parseArgs(raw) {
|
|
8
15
|
try {
|
|
9
16
|
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
@@ -17,6 +24,16 @@ function normalizedInteger(value, fallback) {
|
|
|
17
24
|
const n = Number(value);
|
|
18
25
|
return Number.isFinite(n) ? Math.trunc(n) : fallback;
|
|
19
26
|
}
|
|
27
|
+
/** read_file 实际拿到的行区间:offset 默认 1;limit 默认 300 且钳到 2000(同工具 execute 的归一化)。
|
|
28
|
+
* 无法解析参数时返 null(调用方保守跳过,不做区间淘汰)。 */
|
|
29
|
+
function readRange(argsRaw) {
|
|
30
|
+
const args = parseArgs(argsRaw);
|
|
31
|
+
if (!args)
|
|
32
|
+
return null;
|
|
33
|
+
const offset = Math.max(1, normalizedInteger(args.offset, 1));
|
|
34
|
+
const limit = Math.max(1, Math.min(normalizedInteger(args.limit, READ_DEFAULT_LIMIT), READ_LIMIT_CAP));
|
|
35
|
+
return { offset, limit };
|
|
36
|
+
}
|
|
20
37
|
/** Only complete semantic-query equality is safe for whole-message replacement. */
|
|
21
38
|
function observationKey(call) {
|
|
22
39
|
const args = call.args;
|
|
@@ -46,7 +63,11 @@ function observationLabel(call) {
|
|
|
46
63
|
}
|
|
47
64
|
/**
|
|
48
65
|
* Cross-message relevance pruning:
|
|
49
|
-
* - read_file: a newer successful read of the same canonical path
|
|
66
|
+
* - read_file: a newer successful read of the same canonical path AND the same
|
|
67
|
+
* line range (offset+limit) supersedes old reads; different pages of one file
|
|
68
|
+
* are distinct and never prune each other.
|
|
69
|
+
* - edit_file/write_file: a mutation of a path supersedes that path's old reads
|
|
70
|
+
* across ALL ranges (the whole file changed).
|
|
50
71
|
* - grep: a newer successful call with the exact same semantic arguments
|
|
51
72
|
* supersedes old results. Partial file overlap is intentionally not enough.
|
|
52
73
|
*/
|
|
@@ -114,8 +135,10 @@ export class RelevancePruner {
|
|
|
114
135
|
*/
|
|
115
136
|
pruneSuperseded(history, coldBoundary) {
|
|
116
137
|
try {
|
|
138
|
+
// read→read:键 = path + 区间(offset+limit),只淘汰真正重复的同区间旧 read。
|
|
117
139
|
const latestRead = new Map();
|
|
118
140
|
const latestObservation = new Map();
|
|
141
|
+
// mutation→read:按 path 记录,稍后淘汰该文件所有区间旧 read。
|
|
119
142
|
const mutations = [];
|
|
120
143
|
for (let idx = 1; idx < history.length; idx++) {
|
|
121
144
|
const message = history[idx];
|
|
@@ -127,8 +150,10 @@ export class RelevancePruner {
|
|
|
127
150
|
continue;
|
|
128
151
|
if (call.name === 'read_file') {
|
|
129
152
|
const path = canonicalizePath(extractPath(call.argsRaw));
|
|
130
|
-
|
|
131
|
-
|
|
153
|
+
const range = readRange(call.argsRaw);
|
|
154
|
+
// path 或区间无法确定 → 保守跳过,不做 read→read 淘汰。
|
|
155
|
+
if (path && range)
|
|
156
|
+
latestRead.set(`${path}|${range.offset}|${range.limit}`, { path, range, index: idx });
|
|
132
157
|
}
|
|
133
158
|
else if (call.name === 'edit_file' || call.name === 'write_file') {
|
|
134
159
|
const path = canonicalizePath(extractPath(call.argsRaw));
|
|
@@ -140,11 +165,11 @@ export class RelevancePruner {
|
|
|
140
165
|
latestObservation.set(key, { tool: call.name, index: idx });
|
|
141
166
|
}
|
|
142
167
|
let pruned = 0;
|
|
143
|
-
for (const
|
|
144
|
-
pruned += this.stubPriorReads(history,
|
|
168
|
+
for (const entry of latestRead.values()) {
|
|
169
|
+
pruned += this.stubPriorReads(history, entry, coldBoundary, 'range');
|
|
145
170
|
}
|
|
146
171
|
for (const mutation of mutations) {
|
|
147
|
-
pruned += this.stubPriorReads(history, mutation.path, mutation.index, coldBoundary);
|
|
172
|
+
pruned += this.stubPriorReads(history, { path: mutation.path, index: mutation.index }, coldBoundary, 'mutation');
|
|
148
173
|
}
|
|
149
174
|
for (const [key, latest] of latestObservation) {
|
|
150
175
|
pruned += this.stubPriorObservations(history, latest.tool, key, latest.index, coldBoundary);
|
|
@@ -155,12 +180,16 @@ export class RelevancePruner {
|
|
|
155
180
|
return 0;
|
|
156
181
|
}
|
|
157
182
|
}
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
/**
|
|
184
|
+
* @param mode 'range'(read→read:仅同区间淘汰) | 'mutation'(文件被改:该 path 全区间淘汰)
|
|
185
|
+
*/
|
|
186
|
+
stubPriorReads(history, target, coldBoundary, mode) {
|
|
187
|
+
const targetPath = canonicalizePath(target.path);
|
|
160
188
|
if (!targetPath)
|
|
161
189
|
return 0;
|
|
190
|
+
const reason = mode === 'range' ? READ_RANGE_STUB_REASON : READ_MUTATION_STUB_REASON;
|
|
162
191
|
let pruned = 0;
|
|
163
|
-
for (let idx = 1; idx < Math.min(
|
|
192
|
+
for (let idx = 1; idx < Math.min(target.index, coldBoundary); idx++) {
|
|
164
193
|
const message = history[idx];
|
|
165
194
|
if (!message || message.role !== 'tool')
|
|
166
195
|
continue;
|
|
@@ -168,12 +197,22 @@ export class RelevancePruner {
|
|
|
168
197
|
if (content.startsWith('⌦['))
|
|
169
198
|
continue;
|
|
170
199
|
const call = this.callAt(history, idx);
|
|
171
|
-
if (call?.name !== 'read_file')
|
|
200
|
+
if (call?.name !== 'read_file' || !message.tool_call_id)
|
|
172
201
|
continue;
|
|
173
|
-
if (canonicalizePath(extractPath(call.argsRaw)) !== targetPath
|
|
202
|
+
if (canonicalizePath(extractPath(call.argsRaw)) !== targetPath)
|
|
174
203
|
continue;
|
|
204
|
+
// read→read 必须同区间;mutation→read 不看区间(整文件已变)。
|
|
205
|
+
if (mode === 'range') {
|
|
206
|
+
const priorRange = readRange(call.argsRaw);
|
|
207
|
+
if (!priorRange ||
|
|
208
|
+
!target.range ||
|
|
209
|
+
priorRange.offset !== target.range.offset ||
|
|
210
|
+
priorRange.limit !== target.range.limit) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
175
214
|
message.content =
|
|
176
|
-
`${STUB_PREFIX}${
|
|
215
|
+
`${STUB_PREFIX}${reason}] read_file(${targetPath}) ${content.length} 字符 ` +
|
|
177
216
|
`→ 已被新 read / mutation 替代 · id …${message.tool_call_id.slice(-6)}⌫`;
|
|
178
217
|
pruned++;
|
|
179
218
|
}
|
|
@@ -15,13 +15,11 @@ export const editFileTool = {
|
|
|
15
15
|
name: 'edit_file',
|
|
16
16
|
description: `Replace content in a file transactionally. Two modes:
|
|
17
17
|
|
|
18
|
-
**String replacement (default):** old_string must occur EXACTLY once
|
|
18
|
+
**String replacement (default):** old_string must occur EXACTLY once; copy it verbatim from a fresh read_file output (see Tool policy — never reconstruct it from memory, summaries, or grep output).
|
|
19
19
|
|
|
20
20
|
**Line-range:** line_start/line_end (1-based, inclusive) instead of old_string — for large blocks, repeated patterns, or hard-to-reproduce text.
|
|
21
21
|
|
|
22
|
-
expected_hash (sha256 from read_file artifact header) is required and must match the current file; changed-since-read edits are rejected — re-read and retry with the new hash
|
|
23
|
-
|
|
24
|
-
Anti-patterns (will fail): old_string from memory/summary/stale call; multiple occurrences (add context to disambiguate); hash from another file or old read.`,
|
|
22
|
+
expected_hash (sha256 from the read_file artifact header) is required and must match the current file; changed-since-read edits are rejected — re-read and retry with the new hash.`,
|
|
25
23
|
risk: 'confirm',
|
|
26
24
|
parameters: {
|
|
27
25
|
type: 'object',
|
|
@@ -59,10 +59,8 @@ function renderBodies(lines, lineNos, maxPerFile, context) {
|
|
|
59
59
|
export const grepTool = {
|
|
60
60
|
name: 'grep',
|
|
61
61
|
description: 'Search file contents by regex (recursive, excludes node_modules/.git/dist).\n' +
|
|
62
|
-
'Output: per-file header "<path>: N matches, lines [l1, l2, ...]"
|
|
63
|
-
'Pass context=2..5 to
|
|
64
|
-
'Still use read_file(offset=X, limit=Y) for a whole region or exact edit text — never reconstruct an edit_file old_string from grep output (long lines are clipped). ' +
|
|
65
|
-
'For call chains across many files, prefer the codegraph skill.',
|
|
62
|
+
'Output: per-file header "<path>: N matches, lines [l1, l2, ...]" plus rendered lines with ORIGINAL INDENTATION kept.\n' +
|
|
63
|
+
'Pass context=2..5 to include neighbouring lines inline (like ripgrep -C) instead of following each hit with a read_file round-trip. Use read_file when you need a whole region or exact edit text.',
|
|
66
64
|
parameters: {
|
|
67
65
|
type: 'object',
|
|
68
66
|
properties: {
|
|
@@ -36,11 +36,9 @@ function failure(code, message) {
|
|
|
36
36
|
export const readFileTool = {
|
|
37
37
|
name: 'read_file',
|
|
38
38
|
description: 'Read a file: text with line numbers, images as visual model input.\n' +
|
|
39
|
-
'Text — read before editing.
|
|
40
|
-
'Need several regions of the SAME file? Issue those read_file calls together in one response (they run concurrently) instead of sequential offset+=limit walks. ' +
|
|
39
|
+
'Text — read before editing. For files >500 lines, grep first to locate, then read_file with offset+limit (e.g. offset=350, limit=120); never read a whole large file in one call. ' +
|
|
41
40
|
'Images — PNG/JPEG/GIF/WebP detected by MAGIC BYTES (extension ignored), attached as visual input; detail=low|high controls resolution, oversized PNGs downscale automatically. ' +
|
|
42
|
-
'Other binaries are REJECTED with an explanation — use run_command with a proper tool (`file`, `strings`, disassembler) if you need their content.
|
|
43
|
-
'Architecture/call-chain questions: prefer the codegraph skill over reading files one at a time.',
|
|
41
|
+
'Other binaries are REJECTED with an explanation — use run_command with a proper tool (`file`, `strings`, disassembler) if you need their content.',
|
|
44
42
|
parameters: {
|
|
45
43
|
type: 'object',
|
|
46
44
|
properties: {
|
package/dist/tools/policy.js
CHANGED
|
@@ -237,7 +237,7 @@ export class ToolPolicyController {
|
|
|
237
237
|
'## Tool route (current turn)',
|
|
238
238
|
`Policy ${snapshot.id} v${snapshot.version}; active groups: ${active}.`,
|
|
239
239
|
`Router reason: ${snapshot.reason}`,
|
|
240
|
-
'Use only the exposed tools. Missing capability? Call add_tool_groups alone;
|
|
240
|
+
'Use only the exposed tools. Missing capability? Call add_tool_groups — alone, or batched with read-only tools (read_file/glob/grep/web_search/web_fetch); the read-only calls run in that same step and the new groups take effect on the next step. Do not batch it with write/execute tools, and keep calls that depend on the newly added groups in the next step.',
|
|
241
241
|
];
|
|
242
242
|
if (remaining.length)
|
|
243
243
|
lines.push(`Groups still available: ${remaining.join(', ')}.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 25 个工具,接任意 OpenAI 兼容后端。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"eval:smoke": "tsx evals/smoke.ts && tsx evals/coding/smoke.ts && tsx evals/quality-metrics.ts && tsx evals/work-discipline.ts",
|
|
41
41
|
"eval:coding": "tsx evals/coding/runner.ts",
|
|
42
42
|
"eval:coding:list": "tsx evals/coding/runner.ts --list",
|
|
43
|
-
"prepack": "npm run build"
|
|
43
|
+
"prepack": "npm run build",
|
|
44
|
+
"prepare": "node -e \"try{require('child_process').execFileSync('git',['config','core.hooksPath','.githooks'],{stdio:'ignore'})}catch(e){}\""
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"ajv": "8.20.0",
|