mocode-ai 0.7.0 → 0.7.2
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 +4 -3
- package/README.zh-CN.md +3 -3
- package/dist/__trace_manual_test__.js +1 -0
- package/dist/agent/core.js +621 -260
- package/dist/agent/index.js +20 -0
- package/dist/agent/spawn.js +9 -1
- package/dist/config/index.js +12 -0
- package/dist/i18n/index.js +34 -0
- package/dist/llm/index.js +20 -2
- package/dist/mcp/index.js +15 -2
- package/dist/permissions/index.js +149 -93
- package/dist/repl/index.js +66 -10
- package/dist/rollback/index.js +36 -0
- package/dist/session/index.js +3 -0
- package/dist/session/trace-metrics.js +70 -0
- package/dist/session/trace-sanitize.js +34 -0
- package/dist/session/trace.js +54 -0
- package/dist/tools/builtins/edit-file.js +13 -1
- package/dist/tools/builtins/index.js +42 -4
- package/dist/tools/builtins/run-command.js +115 -66
- package/dist/tools/builtins/task.js +5 -2
- package/dist/tools/builtins/write-file.js +13 -1
- package/dist/tools/constants.js +5 -1
- package/dist/tools/registry.js +141 -39
- package/dist/tools/resource-lock.js +148 -0
- package/dist/verification/affected.js +149 -0
- package/dist/verification/diagnostics.js +108 -0
- package/dist/verification/discovery.js +48 -0
- package/dist/verification/fingerprint.js +54 -0
- package/dist/verification/index.js +333 -0
- package/dist/verification/postconditions.js +98 -0
- package/dist/verification/profile.js +237 -0
- package/dist/verification/targeted-tests.js +96 -0
- package/dist/verification/types.js +1 -0
- package/package.json +5 -2
package/dist/repl/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isProjectSkillEnabled, isProjectSnapshotEnabled, updateProjectSkillConfig, updateSnapshotConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
4
|
+
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, isProjectSkillEnabled, isProjectSnapshotEnabled, updateProjectSkillConfig, updateSnapshotConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
5
5
|
import { getLanguage, normalizeLanguage, t, } from '../i18n/index.js';
|
|
6
6
|
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
7
7
|
import { deletePreset, getPreset, isValidPresetName, listPresets, migrateCurrentToPreset, savePreset, } from '../config/presets.js';
|
|
@@ -16,14 +16,14 @@ import * as mouse from '../ui/mouse.js';
|
|
|
16
16
|
import * as batch from '../ui/batch.js';
|
|
17
17
|
import { promptWithSlashMenu, promptTurnPicker, promptSessionPicker, promptThemePicker, promptRevertChoice, } from '../ui/prompt.js';
|
|
18
18
|
import { promptIntervention } from '../ui/intervention.js';
|
|
19
|
-
import {
|
|
19
|
+
import { registerToolsExtension } from '../tools/registry.js';
|
|
20
20
|
import { initializeAllMcp, getMcpTools, closeAllMcp } from '../mcp/index.js';
|
|
21
|
-
import { estimateMessagesTokens, reconfigureClient, refreshChatTools, } from '../llm/index.js';
|
|
21
|
+
import { estimateMessagesTokens, reconfigureClient, refreshChatTools, chatTools, } from '../llm/index.js';
|
|
22
22
|
import { loadImageAttachment, renderChip, MAX_INLINE_BYTES_DEFAULT, } from '../attachments/image.js';
|
|
23
23
|
import { modelSupportsVision } from '../llm/capabilities.js';
|
|
24
24
|
import { computePruneStats } from '../context/relevance.js';
|
|
25
|
-
import { manualCompact, contextState, newSessionId, saveSession, loadSession, listSessions, } from '../session/index.js';
|
|
26
|
-
import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, } from '../rollback/index.js';
|
|
25
|
+
import { manualCompact, contextState, newSessionId, saveSession, loadSession, listSessions, appendCurrentSessionRuntimeEvent, hashTraceValue, } from '../session/index.js';
|
|
26
|
+
import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, getCurrentTurnId, } from '../rollback/index.js';
|
|
27
27
|
import { listSkills, effectiveSystemPrompt, } from '../skills/index.js';
|
|
28
28
|
import { buildMemorySection, buildMemoryIndexSection, kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, loadAll, } from '../memory/index.js';
|
|
29
29
|
import { buildSnapshot, clearSnapshotCache } from '../project-snapshot/index.js';
|
|
@@ -63,6 +63,13 @@ function buildSlashCommands() {
|
|
|
63
63
|
{ name: 'init', value: '/init', desc: d('commands.memoryInit') },
|
|
64
64
|
],
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
name: '/subagent', desc: d('commands.subagent'), children: [
|
|
68
|
+
{ name: 'on', value: '/subagent on', desc: d('commands.subagentOn') },
|
|
69
|
+
{ name: 'off', value: '/subagent off', desc: d('commands.subagentOff') },
|
|
70
|
+
{ name: 'status', value: '/subagent status', desc: d('commands.subagentStatus') },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
66
73
|
{
|
|
67
74
|
name: '/project_skill', desc: d('commands.skill'), children: [
|
|
68
75
|
{ name: 'toggle', value: '/project_skill', desc: d('commands.toggle') },
|
|
@@ -322,6 +329,8 @@ function runningStateFor(cmd) {
|
|
|
322
329
|
return { status: t('running.memory'), placeholder: t('running.switching') };
|
|
323
330
|
case '/memory_status':
|
|
324
331
|
return { status: t('running.memoryStatus'), placeholder: '…' };
|
|
332
|
+
case '/subagent':
|
|
333
|
+
return { status: t('running.subagent'), placeholder: t('running.switching') };
|
|
325
334
|
case '/project_skill':
|
|
326
335
|
return { status: t('running.skill'), placeholder: t('running.processing') };
|
|
327
336
|
case '/snapshot':
|
|
@@ -387,8 +396,9 @@ function onRunningKey(_str, key) {
|
|
|
387
396
|
runningInput = '';
|
|
388
397
|
layout.paintRunningInputEcho(runningInput, runningPlaceholder);
|
|
389
398
|
}
|
|
390
|
-
else {
|
|
391
|
-
|
|
399
|
+
else if (currentAbort && !currentAbort.signal.aborted) {
|
|
400
|
+
appendCurrentSessionRuntimeEvent('abort', { phase: 'requested', source: 'keyboard' });
|
|
401
|
+
currentAbort.abort();
|
|
392
402
|
}
|
|
393
403
|
return;
|
|
394
404
|
}
|
|
@@ -734,12 +744,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
734
744
|
// 本轮 token 累计:runAgent 返回后写入,供底栏模式 chip 右边显示。undefined=无实测
|
|
735
745
|
// (后端不开 include_usage / 后端失败时)。
|
|
736
746
|
let lastTurnUsage;
|
|
737
|
-
const toolsLine = tools.map((t) => t.name).join(' · ');
|
|
738
747
|
const banner = () => ({
|
|
739
748
|
model: config.model,
|
|
740
749
|
baseURL: config.baseURL,
|
|
741
750
|
cwd: process.cwd(),
|
|
742
|
-
tools:
|
|
751
|
+
tools: chatTools.map((tool) => tool.function.name).join(' · '),
|
|
743
752
|
memoryEnabled: isMemoryEnabled(),
|
|
744
753
|
});
|
|
745
754
|
// 开场:按 config.theme 切主题(横幅 / 状态行 / 后续渲染皆用新色),再进 alt screen + 状态基线 + 清内容区。
|
|
@@ -868,10 +877,22 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
868
877
|
for (const c of revertable)
|
|
869
878
|
revertPaths.add(c.path);
|
|
870
879
|
}
|
|
871
|
-
|
|
880
|
+
const rolledBackFromTurnId = getCurrentTurnId();
|
|
881
|
+
const turnCountBeforeRollback = listTurns().length;
|
|
882
|
+
const rollbackResult = applyRollback(plan, history, revertPaths);
|
|
872
883
|
if (!currentSessionId)
|
|
873
884
|
currentSessionId = newSessionId();
|
|
874
885
|
setCurrentSessionId(currentSessionId, process.cwd()); // 同步到 session/state,确保 notes.md 存在
|
|
886
|
+
appendCurrentSessionRuntimeEvent('rollback', {
|
|
887
|
+
status: 'applied',
|
|
888
|
+
rolledBackFromTurnId,
|
|
889
|
+
cutoffTurnId: plan.cutoffTurnId,
|
|
890
|
+
retainedTurns: plan.n,
|
|
891
|
+
rolledBackTurns: Math.max(0, turnCountBeforeRollback - plan.n),
|
|
892
|
+
deletedMessages: rollbackResult.deletedMsgs,
|
|
893
|
+
revertedFiles: rollbackResult.revertedFiles,
|
|
894
|
+
requestedFileCount: revertPaths.size,
|
|
895
|
+
}, plan.cutoffTurnId);
|
|
875
896
|
try {
|
|
876
897
|
saveSession(history, currentSessionId, queryHistory);
|
|
877
898
|
}
|
|
@@ -1313,6 +1334,15 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1313
1334
|
// 返回 SchedulerRunLog 给 UI 显示决策;退化路径(开关关时)在 manualCompact 内部走 compactHistory。
|
|
1314
1335
|
const log = await manualCompact(history, focus, { force });
|
|
1315
1336
|
const d = log.compactDetail;
|
|
1337
|
+
appendCurrentSessionRuntimeEvent('compact', {
|
|
1338
|
+
source: 'manual',
|
|
1339
|
+
force,
|
|
1340
|
+
called: log.compactHistoryCalled,
|
|
1341
|
+
reason: d?.reason ?? 'unknown',
|
|
1342
|
+
estimateBefore: d?.estimateBefore,
|
|
1343
|
+
estimateAfter: d?.estimateAfter,
|
|
1344
|
+
focusHash: focus ? hashTraceValue(focus) : undefined,
|
|
1345
|
+
});
|
|
1316
1346
|
if (!d) {
|
|
1317
1347
|
// 兜底(旧调用):只显示 old 文案
|
|
1318
1348
|
if (!log.compactHistoryCalled) {
|
|
@@ -1858,6 +1888,32 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1858
1888
|
await rollbackFlow();
|
|
1859
1889
|
continue;
|
|
1860
1890
|
}
|
|
1891
|
+
if (line === '/subagent' || line.startsWith('/subagent ')) {
|
|
1892
|
+
const arg = line.startsWith('/subagent ')
|
|
1893
|
+
? line.slice('/subagent '.length).trim().toLowerCase()
|
|
1894
|
+
: 'status';
|
|
1895
|
+
if (arg === '' || arg === 'status') {
|
|
1896
|
+
const enabled = isSubAgentEnabled();
|
|
1897
|
+
const state = t(enabled ? 'subagent.stateOn' : 'subagent.stateOff');
|
|
1898
|
+
layout.contentWrite(`${ui.accent}${t('subagent.status', { state })}${ui.reset}\n` +
|
|
1899
|
+
`${ui.dim}MOCODE_SUBAGENT_ENABLED=${enabled ? 'true' : 'false'} · ${CONFIG_PATH}${ui.reset}\n`);
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
if (arg !== 'on' && arg !== 'off') {
|
|
1903
|
+
layout.contentWrite(`${ui.yellow}${t('subagent.usage')}${ui.reset}\n`);
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
const enabled = arg === 'on';
|
|
1907
|
+
if (enabled !== isSubAgentEnabled()) {
|
|
1908
|
+
updateSubAgentConfig(enabled);
|
|
1909
|
+
updateConfigKey('MOCODE_SUBAGENT_ENABLED', enabled ? 'true' : 'false');
|
|
1910
|
+
refreshChatTools();
|
|
1911
|
+
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
1912
|
+
layout.rewriteBanner(bannerLines(banner()));
|
|
1913
|
+
}
|
|
1914
|
+
layout.contentWrite(`${enabled ? ui.green : ui.yellow}${t(enabled ? 'subagent.changedOn' : 'subagent.changedOff')}${ui.reset}\n`);
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1861
1917
|
if (line === '/memory_switch' ||
|
|
1862
1918
|
line.startsWith('/memory_switch ') ||
|
|
1863
1919
|
line === '/memory_status' ||
|
package/dist/rollback/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { toText } from '../context/utils.js';
|
|
|
6
6
|
let turnIdCounter = 0;
|
|
7
7
|
let currentTurnId = 0;
|
|
8
8
|
let sequenceCounter = 0;
|
|
9
|
+
/** Monotonic process-local generation; repeated writes to one path still invalidate validation. */
|
|
10
|
+
let mutationVersion = 0;
|
|
9
11
|
let turns = [];
|
|
10
12
|
let snapshots = [];
|
|
11
13
|
const rootDir = () => path.resolve(process.cwd());
|
|
@@ -112,6 +114,11 @@ export function beginTurn(firstLine) {
|
|
|
112
114
|
turnIdCounter += 1;
|
|
113
115
|
currentTurnId = turnIdCounter;
|
|
114
116
|
turns.push({ turnId: currentTurnId, firstLine });
|
|
117
|
+
return currentTurnId;
|
|
118
|
+
}
|
|
119
|
+
/** Stable identity shared by tracing, validation, and rollback for the active main turn. */
|
|
120
|
+
export function getCurrentTurnId() {
|
|
121
|
+
return currentTurnId;
|
|
115
122
|
}
|
|
116
123
|
/** 单路径工具执行前捕获,不立即记账;失败/no-op 不应出现在 rollback 中。 */
|
|
117
124
|
export function beginPathMutation(p) {
|
|
@@ -128,17 +135,22 @@ export function endPathMutation(capture, op) {
|
|
|
128
135
|
const full = safeFullPath(capture.path);
|
|
129
136
|
if (!full)
|
|
130
137
|
return;
|
|
138
|
+
let changed = false;
|
|
131
139
|
const after = readState(full);
|
|
132
140
|
if (!sameState(capture.before, after)) {
|
|
141
|
+
changed = true;
|
|
133
142
|
addSnapshot(snapshotFromState(capture.path, capture.before, capture.sequence, op, capture.createdParents));
|
|
134
143
|
}
|
|
135
144
|
// write_file 会递归创建父目录;即使最终写文件失败,这些目录也是本轮真实副作用。
|
|
136
145
|
for (const parentRel of capture.createdParents) {
|
|
137
146
|
const parent = safeFullPath(parentRel);
|
|
138
147
|
if (parent && readState(parent).kind !== 'missing') {
|
|
148
|
+
changed = true;
|
|
139
149
|
addSnapshot(snapshotFromState(parentRel, { kind: 'missing' }, capture.sequence, op));
|
|
140
150
|
}
|
|
141
151
|
}
|
|
152
|
+
if (changed)
|
|
153
|
+
mutationVersion += 1;
|
|
142
154
|
}
|
|
143
155
|
function isWorkspaceExcluded(full) {
|
|
144
156
|
const base = path.basename(full).toLowerCase();
|
|
@@ -183,13 +195,37 @@ export function beginWorkspaceMutation() {
|
|
|
183
195
|
export function endWorkspaceMutation(capture, op) {
|
|
184
196
|
const after = scanWorkspace();
|
|
185
197
|
const paths = new Set([...capture.entries.keys(), ...after.keys()]);
|
|
198
|
+
let changed = false;
|
|
186
199
|
for (const rel of paths) {
|
|
187
200
|
const beforeState = capture.entries.get(rel) ?? { kind: 'missing' };
|
|
188
201
|
const afterState = after.get(rel) ?? { kind: 'missing' };
|
|
189
202
|
if (sameState(beforeState, afterState))
|
|
190
203
|
continue;
|
|
204
|
+
changed = true;
|
|
191
205
|
addSnapshot(snapshotFromState(rel, beforeState, capture.sequence, op));
|
|
192
206
|
}
|
|
207
|
+
if (changed)
|
|
208
|
+
mutationVersion += 1;
|
|
209
|
+
}
|
|
210
|
+
/** Current main turn changes, deduplicated by path, plus a generation for validation invalidation. */
|
|
211
|
+
export function getCurrentTurnMutationState() {
|
|
212
|
+
const order = [];
|
|
213
|
+
const byPath = new Map();
|
|
214
|
+
for (const snapshot of snapshots) {
|
|
215
|
+
if (snapshot.turnId !== currentTurnId)
|
|
216
|
+
continue;
|
|
217
|
+
let change = byPath.get(snapshot.path);
|
|
218
|
+
if (!change) {
|
|
219
|
+
change = { path: snapshot.path, ops: [], snapshotAvailable: true };
|
|
220
|
+
byPath.set(snapshot.path, change);
|
|
221
|
+
order.push(snapshot.path);
|
|
222
|
+
}
|
|
223
|
+
for (const op of snapshot.ops ?? ['file_change']) {
|
|
224
|
+
if (!change.ops.includes(op))
|
|
225
|
+
change.ops.push(op);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { version: mutationVersion, changedFiles: order.map((item) => byPath.get(item)) };
|
|
193
229
|
}
|
|
194
230
|
export function listTurns() {
|
|
195
231
|
return turns.slice();
|
package/dist/session/index.js
CHANGED
|
@@ -12,3 +12,6 @@ export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, con
|
|
|
12
12
|
export { runScheduler, manualCompact, createBudgetScheduler, } from './scheduler.js';
|
|
13
13
|
export { dropContextFromHistory, formatDropResult, } from './drop.js';
|
|
14
14
|
export { newSessionId, saveSession, loadSession, listSessions, sessionDir, } from './persist.js';
|
|
15
|
+
export { appendCurrentSessionTrace, appendCurrentSessionTraceEvent, appendCurrentSessionRuntimeEvent, createTraceEvent, } from './trace.js';
|
|
16
|
+
export { reduceTraceMetrics, readTraceEvents, readTraceMetrics } from './trace-metrics.js';
|
|
17
|
+
export { summarizeToolArguments, hashTraceValue, safeProviderId } from './trace-sanitize.js';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
export function reduceTraceMetrics(events) {
|
|
3
|
+
const ends = events.filter((event) => event.type === 'tool_call_end');
|
|
4
|
+
let recovered = false;
|
|
5
|
+
let hadFailure = false;
|
|
6
|
+
let successes = 0;
|
|
7
|
+
let toolRetries = 0;
|
|
8
|
+
let tokens = 0;
|
|
9
|
+
let hasTokens = false;
|
|
10
|
+
for (const event of ends) {
|
|
11
|
+
const status = String(event.data.status ?? 'error');
|
|
12
|
+
const retry = Number(event.data.retry ?? 0);
|
|
13
|
+
toolRetries += Number.isFinite(retry) ? retry : 0;
|
|
14
|
+
if (status === 'success') {
|
|
15
|
+
successes++;
|
|
16
|
+
if (hadFailure)
|
|
17
|
+
recovered = true;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
hadFailure = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const event of events) {
|
|
24
|
+
if (event.type !== 'model_end')
|
|
25
|
+
continue;
|
|
26
|
+
const value = event.data.totalTokens;
|
|
27
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
28
|
+
tokens += value;
|
|
29
|
+
hasTokens = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const modelRetries = events.filter((event) => event.type === 'model_retry').length;
|
|
33
|
+
const firstValidation = events.find((event) => event.type === 'validation_end');
|
|
34
|
+
const turnEnd = [...events].reverse().find((event) => event.type === 'turn_end');
|
|
35
|
+
return {
|
|
36
|
+
toolCalls: events.filter((event) => event.type === 'tool_call_start').length,
|
|
37
|
+
toolFailures: ends.length - successes,
|
|
38
|
+
toolRecovery: recovered,
|
|
39
|
+
firstSuccessRate: ends.length ? successes / ends.length : 1,
|
|
40
|
+
modelRetries,
|
|
41
|
+
toolRetries,
|
|
42
|
+
retries: modelRetries + toolRetries,
|
|
43
|
+
tokens: hasTokens ? tokens : null,
|
|
44
|
+
durationMs: Number(turnEnd?.data.durationMs ?? 0),
|
|
45
|
+
firstValidationPassed: firstValidation?.data.status === 'passed',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Reads event JSONL; malformed/legacy summary lines are ignored. */
|
|
49
|
+
export function readTraceEvents(file) {
|
|
50
|
+
const events = [];
|
|
51
|
+
for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
52
|
+
if (!line.trim())
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
const value = JSON.parse(line);
|
|
56
|
+
if (value.schemaVersion === 1 && typeof value.type === 'string' &&
|
|
57
|
+
typeof value.sessionId === 'string' && typeof value.turnId === 'number' &&
|
|
58
|
+
value.data && typeof value.data === 'object') {
|
|
59
|
+
events.push(value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// One corrupt best-effort trace line must not hide the remaining run.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return events;
|
|
67
|
+
}
|
|
68
|
+
export function readTraceMetrics(file) {
|
|
69
|
+
return reduceTraceMetrics(readTraceEvents(file));
|
|
70
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export function hashTraceValue(value) {
|
|
3
|
+
return createHash('sha256').update(value).digest('hex');
|
|
4
|
+
}
|
|
5
|
+
/** Never persists argument values: only shape, size, and a one-way fingerprint. */
|
|
6
|
+
export function summarizeToolArguments(raw) {
|
|
7
|
+
let keys = [];
|
|
8
|
+
let parseable = false;
|
|
9
|
+
try {
|
|
10
|
+
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
11
|
+
parseable = true;
|
|
12
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
13
|
+
keys = Object.keys(parsed).sort();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Invalid arguments are still fingerprinted without retaining their contents.
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
sha256: hashTraceValue(raw),
|
|
21
|
+
byteLength: Buffer.byteLength(raw, 'utf8'),
|
|
22
|
+
keys,
|
|
23
|
+
parseable,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Provider identity is deliberately reduced to a hostname; credentials/path/query are discarded. */
|
|
27
|
+
export function safeProviderId(baseURL) {
|
|
28
|
+
try {
|
|
29
|
+
return new URL(baseURL).hostname.toLowerCase() || 'custom';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 'custom';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { config } from '../config/index.js';
|
|
5
|
+
import { getCurrentTurnId } from '../rollback/index.js';
|
|
6
|
+
import { getCurrentSessionId } from './state.js';
|
|
7
|
+
export function createTraceEvent(input) {
|
|
8
|
+
return {
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
eventId: randomUUID(),
|
|
11
|
+
ts: new Date().toISOString(),
|
|
12
|
+
...input,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function appendTraceLine(sessionId, value) {
|
|
16
|
+
try {
|
|
17
|
+
const dir = path.join(config.sessionDir, sessionId);
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
appendFileSync(path.join(dir, 'trace.jsonl'), `${JSON.stringify(value)}\n`, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Observability is best-effort and cannot block coding work.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Persists a typed event in the current session's append-only black-box log. */
|
|
26
|
+
export function appendCurrentSessionTraceEvent(event) {
|
|
27
|
+
const sessionId = getCurrentSessionId();
|
|
28
|
+
if (!sessionId)
|
|
29
|
+
return;
|
|
30
|
+
appendTraceLine(sessionId, { ...event, sessionId });
|
|
31
|
+
}
|
|
32
|
+
/** Records events initiated outside runAgentCore, such as Ctrl+C, /compact, and /rollback. */
|
|
33
|
+
export function appendCurrentSessionRuntimeEvent(type, data, turnId = getCurrentTurnId()) {
|
|
34
|
+
const sessionId = getCurrentSessionId();
|
|
35
|
+
if (!sessionId)
|
|
36
|
+
return;
|
|
37
|
+
appendTraceLine(sessionId, createTraceEvent({ sessionId, turnId, type, data }));
|
|
38
|
+
}
|
|
39
|
+
/** Legacy turn-summary sink retained for API compatibility. New production code writes events. */
|
|
40
|
+
export function appendCurrentSessionTrace(trace) {
|
|
41
|
+
const sessionId = getCurrentSessionId();
|
|
42
|
+
if (!sessionId)
|
|
43
|
+
return;
|
|
44
|
+
const validation = trace.validation
|
|
45
|
+
? {
|
|
46
|
+
status: trace.validation.status,
|
|
47
|
+
level: trace.validation.level,
|
|
48
|
+
durationMs: trace.validation.durationMs,
|
|
49
|
+
verificationComplete: trace.validation.verificationComplete,
|
|
50
|
+
fingerprint: trace.validation.fingerprint,
|
|
51
|
+
}
|
|
52
|
+
: undefined;
|
|
53
|
+
appendTraceLine(sessionId, { ...trace, sessionId, validation });
|
|
54
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { verifyWrittenFile } from '../../verification/postconditions.js';
|
|
3
4
|
// ---------- edit_file ----------
|
|
4
5
|
export const editFileTool = {
|
|
5
6
|
name: 'edit_file',
|
|
@@ -39,6 +40,17 @@ export const editFileTool = {
|
|
|
39
40
|
// 检测原始行尾风格,写回时还原(存在 \r\n 即视为 CRLF 文件;纯 LF 文件保持 LF)
|
|
40
41
|
const out = data.includes('\r\n') ? updated.replace(/\n/g, '\r\n') : updated;
|
|
41
42
|
await writeFile(full, out, 'utf8');
|
|
42
|
-
|
|
43
|
+
const postcondition = await verifyWrittenFile(full, out);
|
|
44
|
+
if (postcondition.status === 'failed') {
|
|
45
|
+
return {
|
|
46
|
+
status: 'error',
|
|
47
|
+
code: 'POSTCONDITION_FAILED',
|
|
48
|
+
retryable: true,
|
|
49
|
+
output: postcondition.diagnostics
|
|
50
|
+
.map((item) => `[${item.code ?? 'V0_FAILED'}] ${item.file ?? path}: ${item.message}`)
|
|
51
|
+
.join('\n'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return `已在 ${path} 中完成 1 处替换 (sha256=${postcondition.actualHash})。`;
|
|
43
55
|
},
|
|
44
56
|
};
|
|
@@ -46,7 +46,40 @@ const _projectSkillEnabledAtBoot = process.env.MOCODE_PROJECT_SKILL === 'true';
|
|
|
46
46
|
const _projectSkillTools = _projectSkillEnabledAtBoot
|
|
47
47
|
? [projectSkillUpdateTool]
|
|
48
48
|
: [];
|
|
49
|
-
|
|
49
|
+
const pathResource = (args) => typeof args.path === 'string' && args.path ? [`file:${args.path}`] : ['workspace'];
|
|
50
|
+
const workspaceResource = () => ['workspace'];
|
|
51
|
+
const memoryResource = () => ['memory-store'];
|
|
52
|
+
const CAPABILITIES = {
|
|
53
|
+
read_file: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: pathResource },
|
|
54
|
+
write_file: { effect: 'write', concurrency: 'resource-locked', retry: 'never', resources: pathResource },
|
|
55
|
+
edit_file: { effect: 'write', concurrency: 'resource-locked', retry: 'never', resources: pathResource },
|
|
56
|
+
run_command: { effect: 'process', concurrency: 'serial', retry: 'never', resources: workspaceResource, supportsAbort: true },
|
|
57
|
+
glob: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: workspaceResource },
|
|
58
|
+
grep: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: workspaceResource },
|
|
59
|
+
codegraph: { effect: 'read', concurrency: 'parallel', retry: 'safe', resources: workspaceResource, supportsAbort: true },
|
|
60
|
+
web_search: { effect: 'network', concurrency: 'parallel', retry: 'safe', supportsAbort: true },
|
|
61
|
+
web_fetch: { effect: 'network', concurrency: 'parallel', retry: 'safe', supportsAbort: true },
|
|
62
|
+
use_skill: { effect: 'read', concurrency: 'serial', retry: 'safe' },
|
|
63
|
+
ask_human: { effect: 'read', concurrency: 'serial', retry: 'never' },
|
|
64
|
+
switch_mode: { effect: 'write', concurrency: 'serial', retry: 'never', resources: () => ['agent-mode'] },
|
|
65
|
+
drop_context: { effect: 'write', concurrency: 'serial', retry: 'never', resources: () => ['conversation-context'] },
|
|
66
|
+
memory_save: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
67
|
+
memory_search: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
68
|
+
memory_list: { effect: 'read', concurrency: 'serial', retry: 'safe', resources: memoryResource },
|
|
69
|
+
memory_update: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
70
|
+
memory_forget: { effect: 'write', concurrency: 'serial', retry: 'never', resources: memoryResource },
|
|
71
|
+
project_skill_update: { effect: 'write', concurrency: 'serial', retry: 'never', resources: workspaceResource },
|
|
72
|
+
// task 只编排子 Agent;真实读写由子调用自行持锁,父调用不得包 workspace 锁。
|
|
73
|
+
task: {
|
|
74
|
+
effect: 'write',
|
|
75
|
+
concurrency: 'serial',
|
|
76
|
+
retry: 'never',
|
|
77
|
+
resources: workspaceResource,
|
|
78
|
+
delegatesResourceLocks: true,
|
|
79
|
+
supportsAbort: true,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const rawBuiltinTools = [
|
|
50
83
|
readFileTool,
|
|
51
84
|
writeFileTool,
|
|
52
85
|
editFileTool,
|
|
@@ -58,9 +91,14 @@ export const builtinTools = [
|
|
|
58
91
|
webFetchTool,
|
|
59
92
|
useSkillTool,
|
|
60
93
|
askHumanTool,
|
|
61
|
-
switchModeTool,
|
|
62
|
-
dropContextTool,
|
|
94
|
+
switchModeTool,
|
|
95
|
+
dropContextTool,
|
|
63
96
|
..._memoryTools,
|
|
64
97
|
..._projectSkillTools,
|
|
65
|
-
taskTool,
|
|
98
|
+
taskTool,
|
|
66
99
|
];
|
|
100
|
+
/** 所有内置工具均携带显式能力;新增工具遗漏声明时 registry 会保守串行。 */
|
|
101
|
+
export const builtinTools = rawBuiltinTools.map((tool) => ({
|
|
102
|
+
...tool,
|
|
103
|
+
capabilities: CAPABILITIES[tool.name],
|
|
104
|
+
}));
|