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.
@@ -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 { tools, registerToolsExtension } from '../tools/registry.js';
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
- currentAbort?.abort();
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: toolsLine,
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
- applyRollback(plan, history, revertPaths);
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' ||
@@ -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();
@@ -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
- return `已在 ${path} 中完成 1 处替换。`;
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
- export const builtinTools = [
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, // plan↔auto 自切(两模式都可见,不进 PLAN_DISABLED_TOOLS;副作用控制工具→串行分支)
62
- dropContextTool, // 运行中剔除无关 tool 结果(上下文管理,无副作用;两模式都可见,串行分支)
94
+ switchModeTool,
95
+ dropContextTool,
63
96
  ..._memoryTools,
64
97
  ..._projectSkillTools,
65
- taskTool, // 派生子 agent(独立 history + 可受限工具集);plan 模式禁用(见 PLAN_DISABLED_TOOLS)
98
+ taskTool,
66
99
  ];
100
+ /** 所有内置工具均携带显式能力;新增工具遗漏声明时 registry 会保守串行。 */
101
+ export const builtinTools = rawBuiltinTools.map((tool) => ({
102
+ ...tool,
103
+ capabilities: CAPABILITIES[tool.name],
104
+ }));