mocode-ai 0.7.2 → 1.0.1
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 +58 -4
- package/README.zh-CN.md +58 -4
- package/dist/agent/core.js +119 -39
- package/dist/agent/index.js +57 -6
- package/dist/agent/spawn.js +74 -30
- package/dist/agents/coordinator.js +60 -0
- package/dist/changeset/index.js +289 -0
- package/dist/changeset/types.js +1 -0
- package/dist/config/index.js +25 -13
- package/dist/context/artifacts.js +254 -0
- package/dist/context/classifier.js +1 -1
- package/dist/context/index.js +1 -0
- package/dist/i18n/index.js +16 -4
- package/dist/llm/index.js +2 -2
- package/dist/permissions/index.js +7 -1
- package/dist/repl/index.js +31 -5
- package/dist/rollback/index.js +43 -8
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +2 -2
- package/dist/sandbox/root.js +9 -3
- package/dist/session/compact.js +9 -3
- package/dist/session/scheduler.js +4 -0
- package/dist/session/state.js +3 -19
- package/dist/tools/builtins/apply-patch.js +174 -0
- package/dist/tools/builtins/edit-file.js +60 -37
- package/dist/tools/builtins/index.js +10 -8
- package/dist/tools/builtins/read-file.js +4 -2
- package/dist/tools/builtins/task.js +47 -16
- package/dist/tools/builtins/web-fetch.js +21 -5
- package/dist/tools/builtins/web-search.js +28 -4
- package/dist/tools/builtins/write-file.js +53 -18
- package/dist/tools/constants.js +3 -3
- package/dist/tools/registry.js +108 -49
- package/dist/tools/retry.js +105 -0
- package/dist/tools/validation.js +80 -0
- package/dist/ui/batch.js +56 -24
- package/dist/ui/layout.js +8 -3
- package/package.json +2 -1
package/dist/config/index.js
CHANGED
|
@@ -113,8 +113,7 @@ export function isProjectSnapshotEnabled() {
|
|
|
113
113
|
* Agent 用 write_file/edit_file/read_file 维护此文件,抗 compact(在 context window 之外)。
|
|
114
114
|
* 文件不存在或为空时返空串(零开销)。
|
|
115
115
|
*/
|
|
116
|
-
function buildNotepadSection() {
|
|
117
|
-
const sessionId = getCurrentSessionId();
|
|
116
|
+
function buildNotepadSection(sessionId = getCurrentSessionId()) {
|
|
118
117
|
if (!sessionId)
|
|
119
118
|
return '';
|
|
120
119
|
const root = getSandboxRoot() ?? process.cwd();
|
|
@@ -193,7 +192,7 @@ You are in PLAN mode: investigate and design only — do NOT execute or change a
|
|
|
193
192
|
${PLAN_RESEARCH_RULES}`;
|
|
194
193
|
}
|
|
195
194
|
/** 兼容旧名字:repl 的 buildSystemMessage 仍引 PLAN_MODE_SUFFIX(变量)。运行时按需现拼。 */
|
|
196
|
-
export function buildBasePrompt() {
|
|
195
|
+
export function buildBasePrompt(sessionId = getCurrentSessionId()) {
|
|
197
196
|
const autoAllToolsLine = isMemoryEnabled()
|
|
198
197
|
? '- Default is AUTO mode: you research and execute with all tools (read/edit/run_command/memory/web/skills).'
|
|
199
198
|
: '- Default is AUTO mode: you research and execute with all tools (read/edit/run_command/web/skills).';
|
|
@@ -219,7 +218,7 @@ ${PLATFORM_NOTE}
|
|
|
219
218
|
- Batch only independent read-only calls. After their results arrive, make the dependent edit in the next turn; then batch independent edits and one final verification when their exact inputs are already known.
|
|
220
219
|
- Read only what supports the next decision; verify once after a related edit set, not after every edit.
|
|
221
220
|
- Do not repeat an unchanged failing call; after three unproductive attempts, change tools or ask for the missing decision.
|
|
222
|
-
- For \`edit_file\`, derive \`old_string\` by copying the exact relevant lines from the latest successful \`read_file\` of that same path
|
|
221
|
+
- For \`edit_file\`, derive \`old_string\` by copying the exact relevant lines from the latest successful \`read_file\` of that same path and pass that read's \`expected_hash\`; never reconstruct either from memory, a summary, grep output, or a previous diff. That read becomes stale after any edit/write to the path, compaction/resume, or a possible external change. On a conflict, re-read the exact region and retry once with the new text and hash; never retry identical arguments.
|
|
223
222
|
|
|
224
223
|
## Workflow
|
|
225
224
|
- Understand requirements and current code before acting; do not guess.
|
|
@@ -229,7 +228,7 @@ ${PLATFORM_NOTE}
|
|
|
229
228
|
|
|
230
229
|
## Tool rules
|
|
231
230
|
- Precise path/symbol → go directly to \`read_file\` or \`codegraph node\`; use \`glob\`/\`grep\` only for discovery.
|
|
232
|
-
- Before editing, read the exact target region and
|
|
231
|
+
- Before editing, read the exact target region and copy both its artifact \`hash\` and verbatim text. Use \`edit_file\` with \`expected_hash\` for unique local replacements, and \`write_file\` with the latest hash for replacement (or null only for creation).
|
|
233
232
|
- Local edits require an exact unique match; use \`write_file\` for new/full files.
|
|
234
233
|
- Use \`glob\`/\`grep\` for discovery and \`run_command\` for execution or verification, not file existence checks. State intent before side effects.
|
|
235
234
|
- Call \`ask_human\` only when a real user decision is required; otherwise decide and proceed.
|
|
@@ -252,11 +251,11 @@ ${PLATFORM_NOTE}
|
|
|
252
251
|
- Operate only within authorized scope; when unsure, ask — don't guess.
|
|
253
252
|
|
|
254
253
|
## Project context (dynamic reference)
|
|
255
|
-
${buildSnapshotSection()}${config.projectSkillEnabled ? buildProjectSkillSection() : ''}${memorySection}${buildNotepadSection()}
|
|
254
|
+
${buildSnapshotSection()}${config.projectSkillEnabled ? buildProjectSkillSection() : ''}${memorySection}${buildNotepadSection(sessionId)}
|
|
256
255
|
|
|
257
256
|
## Session Notepad — working notes file
|
|
258
|
-
${
|
|
259
|
-
? `You maintain a working notepad at \`.mocode/sessions/${
|
|
257
|
+
${sessionId
|
|
258
|
+
? `You maintain a working notepad at \`.mocode/sessions/${sessionId}/notes.md\` using write_file / edit_file / read_file.`
|
|
260
259
|
: 'You maintain a working notepad (path will be shown after the session starts).'}
|
|
261
260
|
This is your private working surface — write intermediate findings, decisions, open questions,
|
|
262
261
|
and anything you might need to recall later. The file survives context compaction.
|
|
@@ -296,8 +295,8 @@ Example:
|
|
|
296
295
|
- [ ] Check if rate limiter interacts with auth middleware
|
|
297
296
|
|
|
298
297
|
### RULES
|
|
299
|
-
${
|
|
300
|
-
? `- Your notepad file path is: \`.mocode/sessions/${
|
|
298
|
+
${sessionId
|
|
299
|
+
? `- Your notepad file path is: \`.mocode/sessions/${sessionId}/notes.md\`. Use this exact path for all read_file/write_file/edit_file operations on your notes.`
|
|
301
300
|
: '- Your notepad file path will be available after the session starts.'}
|
|
302
301
|
- Use write_file to create/overwrite; use edit_file to append or modify sections
|
|
303
302
|
- Keep the file concise — summarize, don't dump raw tool output
|
|
@@ -331,6 +330,19 @@ Rules:
|
|
|
331
330
|
- **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
|
|
332
331
|
- Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.`;
|
|
333
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Stable, production-grade behavior shared by main and sub agents.
|
|
335
|
+
* It intentionally excludes session/project payload (snapshot, memory index, notepad), while
|
|
336
|
+
* retaining the exact editing, verification, recovery, safety, and reporting rules.
|
|
337
|
+
*/
|
|
338
|
+
export function buildMocodeCorePrompt() {
|
|
339
|
+
const full = buildBasePrompt();
|
|
340
|
+
const dynamicStart = full.indexOf('## Project context (dynamic reference)');
|
|
341
|
+
const reportingStart = full.indexOf('## Termination & Reporting');
|
|
342
|
+
if (dynamicStart < 0 || reportingStart < dynamicStart)
|
|
343
|
+
return full;
|
|
344
|
+
return `${full.slice(0, dynamicStart).trimEnd()}\n\n${full.slice(reportingStart)}`;
|
|
345
|
+
}
|
|
334
346
|
/**
|
|
335
347
|
* plan 模式追加到系统提示末尾的指令。
|
|
336
348
|
* 历史曾是 `export const PLAN_MODE_SUFFIX`(顶层字面量);现改为按 isMemoryEnabled()
|
|
@@ -366,9 +378,9 @@ export const config = {
|
|
|
366
378
|
autoReflect: process.env.AUTO_REFLECT !== 'false',
|
|
367
379
|
memoryEnabled: process.env.MEMORY_ENABLED === 'true',
|
|
368
380
|
reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
|
|
369
|
-
maxSteps: Number(process.env.MAX_STEPS) ||
|
|
381
|
+
maxSteps: Number(process.env.MAX_STEPS) || 1000,
|
|
370
382
|
subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
|
|
371
|
-
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) ||
|
|
383
|
+
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || Number(process.env.MAX_STEPS) || 1000,
|
|
372
384
|
sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
|
|
373
385
|
searchApiKey: process.env.ANYSEARCH_API_KEY,
|
|
374
386
|
sandboxRoot: process.env.SANDBOX_ROOT || undefined,
|
|
@@ -410,7 +422,7 @@ export function updateModelConfig(opts) {
|
|
|
410
422
|
process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
|
|
411
423
|
}
|
|
412
424
|
}
|
|
413
|
-
/** 子 Agent 总开关;默认 false,关闭时
|
|
425
|
+
/** 子 Agent 总开关;默认 false,关闭时 sub-agent 不进入模型工具表。 */
|
|
414
426
|
export function isSubAgentEnabled() {
|
|
415
427
|
return config.subAgentEnabled;
|
|
416
428
|
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { estimateTokens } from '../llm/index.js';
|
|
4
|
+
import { canonicalizePath, extractPath, toText } from './utils.js';
|
|
5
|
+
const states = new WeakMap();
|
|
6
|
+
const STALE_PREFIX = '⌦[stale artifact:';
|
|
7
|
+
const ARTIFACT_HEADER = /^\[artifact\s+([^\]]+)\]\n?/;
|
|
8
|
+
function stateFor(state) {
|
|
9
|
+
let current = states.get(state);
|
|
10
|
+
if (!current) {
|
|
11
|
+
current = { artifacts: new Map() };
|
|
12
|
+
states.set(state, current);
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
function parseArgs(raw) {
|
|
17
|
+
try {
|
|
18
|
+
const value = JSON.parse(raw || '{}');
|
|
19
|
+
return value && typeof value === 'object' ? value : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function callArgs(history, idx) {
|
|
26
|
+
const id = history[idx].tool_call_id;
|
|
27
|
+
if (!id)
|
|
28
|
+
return null;
|
|
29
|
+
for (let cursor = idx - 1; cursor >= 0; cursor--) {
|
|
30
|
+
const message = history[cursor];
|
|
31
|
+
if (message.role !== 'assistant')
|
|
32
|
+
continue;
|
|
33
|
+
const calls = message.tool_calls;
|
|
34
|
+
const hit = calls?.find((call) => call.id === id);
|
|
35
|
+
if (hit?.function?.name)
|
|
36
|
+
return { tool: hit.function.name, argsRaw: hit.function.arguments ?? '{}' };
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function sourceType(tool) {
|
|
41
|
+
if (tool === 'read_file')
|
|
42
|
+
return 'read';
|
|
43
|
+
if (tool === 'grep' || tool === 'glob' || tool === 'codegraph')
|
|
44
|
+
return 'search';
|
|
45
|
+
if (tool === 'run_command')
|
|
46
|
+
return 'diagnostic';
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function pathsFromOutput(tool, output) {
|
|
50
|
+
const paths = new Set();
|
|
51
|
+
const add = (value) => {
|
|
52
|
+
const normalized = canonicalizePath(value);
|
|
53
|
+
if (normalized)
|
|
54
|
+
paths.add(normalized);
|
|
55
|
+
};
|
|
56
|
+
if (tool === 'grep' || tool === 'codegraph' || tool === 'run_command') {
|
|
57
|
+
const expression = /^(.+?\.[A-Za-z0-9]+):(?:\d+|\s*\d+\s*(?:处匹配|matches?))/gmi;
|
|
58
|
+
let match;
|
|
59
|
+
while ((match = expression.exec(output)))
|
|
60
|
+
add(match[1]);
|
|
61
|
+
}
|
|
62
|
+
else if (tool === 'glob') {
|
|
63
|
+
for (const line of output.split(/\r?\n/)) {
|
|
64
|
+
const value = line.trim();
|
|
65
|
+
if (value && !value.includes(' ') && (value.includes('/') || value.includes('\\')))
|
|
66
|
+
add(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...paths];
|
|
70
|
+
}
|
|
71
|
+
function parseReadHash(output) {
|
|
72
|
+
return /\bhash=(sha256:[a-f0-9]{64})\b/i.exec(output)?.[1]?.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
function currentFileHash(file) {
|
|
75
|
+
if (file === '*')
|
|
76
|
+
return undefined;
|
|
77
|
+
try {
|
|
78
|
+
return `sha256:${createHash('sha256').update(readFileSync(file)).digest('hex')}`;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function updateStats(state, artifactState) {
|
|
85
|
+
const stats = { fresh: 0, stale: 0, stubbed: 0, tokensBySource: {} };
|
|
86
|
+
for (const artifact of artifactState.artifacts.values()) {
|
|
87
|
+
stats[artifact.freshness] += 1;
|
|
88
|
+
stats.tokensBySource[artifact.source.type] =
|
|
89
|
+
(stats.tokensBySource[artifact.source.type] ?? 0) + artifact.tokenCount;
|
|
90
|
+
}
|
|
91
|
+
state.artifactStats = stats;
|
|
92
|
+
}
|
|
93
|
+
export function recordArtifact(state, history, idx, output, succeeded) {
|
|
94
|
+
if (!succeeded)
|
|
95
|
+
return;
|
|
96
|
+
const call = callArgs(history, idx);
|
|
97
|
+
if (!call)
|
|
98
|
+
return;
|
|
99
|
+
const type = sourceType(call.tool);
|
|
100
|
+
if (!type)
|
|
101
|
+
return;
|
|
102
|
+
const args = parseArgs(call.argsRaw);
|
|
103
|
+
const id = history[idx].tool_call_id ?? `${idx}`;
|
|
104
|
+
const directPath = canonicalizePath(extractPath(call.argsRaw));
|
|
105
|
+
let dependencies = directPath
|
|
106
|
+
? [{ path: directPath }]
|
|
107
|
+
: pathsFromOutput(call.tool, output).map((path) => ({ path }));
|
|
108
|
+
// Diagnostics and searches with no parseable result conservatively depend on the workspace.
|
|
109
|
+
if (dependencies.length === 0 && type !== 'read')
|
|
110
|
+
dependencies = [{ path: '*' }];
|
|
111
|
+
// Capture dependency versions now; future scheduler steps can detect external changes.
|
|
112
|
+
dependencies = dependencies.map((dependency) => {
|
|
113
|
+
const hashAtCreation = currentFileHash(dependency.path);
|
|
114
|
+
return { ...dependency, ...(hashAtCreation ? { hash: hashAtCreation } : {}) };
|
|
115
|
+
});
|
|
116
|
+
const hash = type === 'read' ? parseReadHash(output) : undefined;
|
|
117
|
+
if (hash && dependencies[0])
|
|
118
|
+
dependencies[0].hash = hash;
|
|
119
|
+
const content = toText(history[idx].content);
|
|
120
|
+
const artifact = {
|
|
121
|
+
id,
|
|
122
|
+
source: { type, tool: call.tool, toolCallId: id },
|
|
123
|
+
...(hash ? { hash } : {}),
|
|
124
|
+
version: Date.now(),
|
|
125
|
+
dependencies,
|
|
126
|
+
freshness: content.startsWith('⌦[') ? 'stubbed' : 'fresh',
|
|
127
|
+
rebuildable: true,
|
|
128
|
+
tokenCount: estimateTokens(content),
|
|
129
|
+
messageIndex: idx,
|
|
130
|
+
};
|
|
131
|
+
stateFor(state).artifacts.set(id, artifact);
|
|
132
|
+
updateStats(state, stateFor(state));
|
|
133
|
+
}
|
|
134
|
+
function affected(artifact, changed) {
|
|
135
|
+
return artifact.dependencies.some((dependency) => dependency.path === '*' || changed.has(dependency.path));
|
|
136
|
+
}
|
|
137
|
+
/** Mark and immediately stub stale facts; this is stronger than waiting for budget pressure. */
|
|
138
|
+
export function invalidateArtifacts(state, history, changedFiles) {
|
|
139
|
+
const changed = new Set(changedFiles.map(canonicalizePath).filter((item) => !!item));
|
|
140
|
+
if (changed.size === 0)
|
|
141
|
+
return 0;
|
|
142
|
+
const artifactState = stateFor(state);
|
|
143
|
+
let count = 0;
|
|
144
|
+
for (const artifact of artifactState.artifacts.values()) {
|
|
145
|
+
if (artifact.freshness !== 'fresh' || !affected(artifact, changed))
|
|
146
|
+
continue;
|
|
147
|
+
artifact.freshness = 'stale';
|
|
148
|
+
const message = history[artifact.messageIndex];
|
|
149
|
+
if (message?.role === 'tool') {
|
|
150
|
+
const original = toText(message.content);
|
|
151
|
+
const paths = artifact.dependencies.map((item) => item.path).join(', ');
|
|
152
|
+
const stub = `${STALE_PREFIX}${artifact.source.tool}] source=${artifact.id} dependencies=${paths} ` +
|
|
153
|
+
`invalidated-by=${[...changed].join(', ')}; re-run ${artifact.source.tool} before using this fact.`;
|
|
154
|
+
message.content = stub;
|
|
155
|
+
artifact.tokenCount = estimateTokens(stub);
|
|
156
|
+
}
|
|
157
|
+
count++;
|
|
158
|
+
}
|
|
159
|
+
updateStats(state, artifactState);
|
|
160
|
+
return count;
|
|
161
|
+
}
|
|
162
|
+
/** Rebuild indices after resume/compaction while retaining versions for surviving tool_call IDs. */
|
|
163
|
+
export function rehydrateArtifacts(state, history) {
|
|
164
|
+
const artifactState = stateFor(state);
|
|
165
|
+
const previous = new Map(artifactState.artifacts);
|
|
166
|
+
artifactState.artifacts.clear();
|
|
167
|
+
for (let idx = 0; idx < history.length; idx++) {
|
|
168
|
+
const message = history[idx];
|
|
169
|
+
if (message.role !== 'tool')
|
|
170
|
+
continue;
|
|
171
|
+
const call = callArgs(history, idx);
|
|
172
|
+
if (!call || !sourceType(call.tool))
|
|
173
|
+
continue;
|
|
174
|
+
const content = toText(message.content);
|
|
175
|
+
const id = message.tool_call_id ?? `${idx}`;
|
|
176
|
+
const retained = previous.get(id);
|
|
177
|
+
if (retained) {
|
|
178
|
+
retained.messageIndex = idx;
|
|
179
|
+
retained.tokenCount = estimateTokens(content);
|
|
180
|
+
retained.freshness = content.startsWith(STALE_PREFIX)
|
|
181
|
+
? 'stale'
|
|
182
|
+
: content.startsWith('⌦[') ? 'stubbed' : retained.freshness;
|
|
183
|
+
artifactState.artifacts.set(id, retained);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
recordArtifact(state, history, idx, content, true);
|
|
187
|
+
const artifact = artifactState.artifacts.get(id);
|
|
188
|
+
if (artifact && content.startsWith(STALE_PREFIX))
|
|
189
|
+
artifact.freshness = 'stale';
|
|
190
|
+
else if (artifact && content.startsWith('⌦['))
|
|
191
|
+
artifact.freshness = 'stubbed';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
updateStats(state, artifactState);
|
|
195
|
+
}
|
|
196
|
+
/** Compare captured dependency versions before each model step to detect external edits. */
|
|
197
|
+
export function refreshArtifactFreshness(state, history) {
|
|
198
|
+
const changed = new Set();
|
|
199
|
+
for (const artifact of stateFor(state).artifacts.values()) {
|
|
200
|
+
if (artifact.freshness !== 'fresh')
|
|
201
|
+
continue;
|
|
202
|
+
for (const dependency of artifact.dependencies) {
|
|
203
|
+
if (!dependency.hash || dependency.path === '*')
|
|
204
|
+
continue;
|
|
205
|
+
if (currentFileHash(dependency.path) !== dependency.hash)
|
|
206
|
+
changed.add(dependency.path);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return changed.size > 0 ? invalidateArtifacts(state, history, [...changed]) : 0;
|
|
210
|
+
}
|
|
211
|
+
/** Scheduler entry point: stale artifacts are already stubs; normalize any resumed stale message first. */
|
|
212
|
+
export function pruneStaleArtifacts(state, history) {
|
|
213
|
+
const artifactState = stateFor(state);
|
|
214
|
+
let pruned = 0;
|
|
215
|
+
for (const artifact of artifactState.artifacts.values()) {
|
|
216
|
+
if (artifact.freshness !== 'stale')
|
|
217
|
+
continue;
|
|
218
|
+
const message = history[artifact.messageIndex];
|
|
219
|
+
if (message?.role !== 'tool')
|
|
220
|
+
continue;
|
|
221
|
+
const content = toText(message.content);
|
|
222
|
+
if (!content.startsWith(STALE_PREFIX)) {
|
|
223
|
+
message.content = `${STALE_PREFIX}${artifact.source.tool}] source=${artifact.id}; re-run before use.`;
|
|
224
|
+
artifact.tokenCount = estimateTokens(String(message.content));
|
|
225
|
+
pruned++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
updateStats(state, artifactState);
|
|
229
|
+
return pruned;
|
|
230
|
+
}
|
|
231
|
+
export function collectArtifactRefs(messages) {
|
|
232
|
+
const refs = new Set();
|
|
233
|
+
for (const message of messages) {
|
|
234
|
+
if (message.role !== 'tool')
|
|
235
|
+
continue;
|
|
236
|
+
const id = message.tool_call_id;
|
|
237
|
+
if (id)
|
|
238
|
+
refs.add(id);
|
|
239
|
+
const content = toText(message.content);
|
|
240
|
+
const hash = parseReadHash(content);
|
|
241
|
+
if (hash)
|
|
242
|
+
refs.add(hash);
|
|
243
|
+
}
|
|
244
|
+
return [...refs].slice(0, 24);
|
|
245
|
+
}
|
|
246
|
+
export function formatArtifactTokenSources(stats) {
|
|
247
|
+
if (!stats)
|
|
248
|
+
return 'none';
|
|
249
|
+
const entries = Object.entries(stats.tokensBySource)
|
|
250
|
+
.filter((entry) => typeof entry[1] === 'number' && entry[1] > 0)
|
|
251
|
+
.sort((left, right) => right[1] - left[1]);
|
|
252
|
+
return entries.length > 0 ? entries.map(([source, tokens]) => `${source} ${tokens}`).join(' · ') : 'none';
|
|
253
|
+
}
|
|
254
|
+
export { STALE_PREFIX, ARTIFACT_HEADER };
|
package/dist/context/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// (叶子级:仅 stdlib + tools/constants + session/compact 的 capToolResultForHistory 兜底 + config 开关)。
|
|
7
7
|
export { optimizeToolResult } from './pipeline.js';
|
|
8
8
|
export { classify, knownToolKinds } from './classifier.js';
|
|
9
|
+
export { recordArtifact, invalidateArtifacts, rehydrateArtifacts, refreshArtifactFreshness, pruneStaleArtifacts, collectArtifactRefs, formatArtifactTokenSources, } from './artifacts.js';
|
|
9
10
|
export { registerEncoder, registerAll, getEncoder, registeredKinds, } from './registry.js';
|
|
10
11
|
// ── Context Budget Scheduler ───────────────────────────────────────────────
|
|
11
12
|
export { evaluateBudget, scheduleActions, formatReport, quickEstimate, userTurnBoundary, BUDGET_LAYERS, DEFAULT_BUDGET_POLICY, BUDGET_RATIO, HOT_TURN_WINDOW, TOOL_OLD_AGE, } from './budget.js';
|
package/dist/i18n/index.js
CHANGED
|
@@ -169,6 +169,12 @@ const zhCN = {
|
|
|
169
169
|
'agent.validationNoCommand': '未发现验证命令',
|
|
170
170
|
'agent.validationResult': '自动验证 {command} → {status}',
|
|
171
171
|
'agent.workedFor': '耗时 {elapsed}',
|
|
172
|
+
'agent.toolsRunning': '正在探索',
|
|
173
|
+
'agent.toolsComplete': '探索',
|
|
174
|
+
'agent.toolsFailed': '探索失败',
|
|
175
|
+
'agent.changes': '文件变更',
|
|
176
|
+
'agent.files': '{count} 个文件',
|
|
177
|
+
'agent.complete': '完成',
|
|
172
178
|
'toolSummary.lines': '{count} 行',
|
|
173
179
|
'toolSummary.files': '{count} 个文件',
|
|
174
180
|
'toolSummary.matches': '{count} 处匹配',
|
|
@@ -212,8 +218,8 @@ const zhCN = {
|
|
|
212
218
|
'subagent.status': '子 Agent:{state}',
|
|
213
219
|
'subagent.stateOn': '开启',
|
|
214
220
|
'subagent.stateOff': '关闭',
|
|
215
|
-
'subagent.changedOn': '已开启子 Agent;
|
|
216
|
-
'subagent.changedOff': '已关闭子 Agent;
|
|
221
|
+
'subagent.changedOn': '已开启子 Agent;sub-agent 将从下一次模型请求起可用。',
|
|
222
|
+
'subagent.changedOff': '已关闭子 Agent;sub-agent 已从模型工具表移除。',
|
|
217
223
|
'subagent.usage': '用法:/subagent on|off|status',
|
|
218
224
|
'plan.ready': '计划已就绪',
|
|
219
225
|
'plan.approvalDetail': '切换到 auto 模式按上述计划执行?(plan 模式只读探查,执行需切 auto)',
|
|
@@ -395,6 +401,12 @@ const en = {
|
|
|
395
401
|
'agent.validationNoCommand': 'no validation command',
|
|
396
402
|
'agent.validationResult': 'Automatic validation {command} → {status}',
|
|
397
403
|
'agent.workedFor': 'Worked for {elapsed}',
|
|
404
|
+
'agent.toolsRunning': 'Exploring',
|
|
405
|
+
'agent.toolsComplete': 'Exploration',
|
|
406
|
+
'agent.toolsFailed': 'Exploration failed',
|
|
407
|
+
'agent.changes': 'Changes',
|
|
408
|
+
'agent.files': '{count} file(s)',
|
|
409
|
+
'agent.complete': 'Complete',
|
|
398
410
|
'toolSummary.lines': '{count} lines',
|
|
399
411
|
'toolSummary.files': '{count} files',
|
|
400
412
|
'toolSummary.matches': '{count} matches',
|
|
@@ -438,8 +450,8 @@ const en = {
|
|
|
438
450
|
'subagent.status': 'Sub-agent: {state}',
|
|
439
451
|
'subagent.stateOn': 'enabled',
|
|
440
452
|
'subagent.stateOff': 'disabled',
|
|
441
|
-
'subagent.changedOn': 'Sub-agents enabled;
|
|
442
|
-
'subagent.changedOff': 'Sub-agents disabled;
|
|
453
|
+
'subagent.changedOn': 'Sub-agents enabled; sub-agent will be available from the next model request.',
|
|
454
|
+
'subagent.changedOff': 'Sub-agents disabled; sub-agent has been removed from the model tool list.',
|
|
443
455
|
'subagent.usage': 'Usage: /subagent on|off|status',
|
|
444
456
|
'plan.ready': 'Plan ready',
|
|
445
457
|
'plan.approvalDetail': 'Switch to auto mode and execute the plan above? (Plan mode is read-only; execution requires auto mode.)',
|
package/dist/llm/index.js
CHANGED
|
@@ -149,10 +149,10 @@ export function __setChatCreateImpl(impl) {
|
|
|
149
149
|
export const chatTools = [];
|
|
150
150
|
export const planChatTools = [];
|
|
151
151
|
export function refreshChatTools() {
|
|
152
|
-
//
|
|
152
|
+
// sub-agent 常驻内部 registry,运行时开关只控制模型可见 schema,因而 on/off 可即时生效。
|
|
153
153
|
const visibleTools = isSubAgentEnabled()
|
|
154
154
|
? tools
|
|
155
|
-
: tools.filter((tool) => tool.name !== '
|
|
155
|
+
: tools.filter((tool) => tool.name !== 'sub-agent');
|
|
156
156
|
const next = visibleTools.map((t) => ({
|
|
157
157
|
type: 'function',
|
|
158
158
|
function: {
|
|
@@ -33,7 +33,13 @@ export function permissionFingerprint(tool, args) {
|
|
|
33
33
|
subject = { command: args.command.trim() };
|
|
34
34
|
}
|
|
35
35
|
else {
|
|
36
|
-
|
|
36
|
+
let resources;
|
|
37
|
+
try {
|
|
38
|
+
resources = tool.capabilities?.resources?.(args).filter(Boolean).sort();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
resources = undefined;
|
|
42
|
+
}
|
|
37
43
|
// File mutations may be granted by their concrete resource. Coarse resources such as
|
|
38
44
|
// "workspace" must retain arguments so task/process-like calls cannot become tool-wide.
|
|
39
45
|
subject = resources?.length && typeof args.path === 'string'
|
package/dist/repl/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { estimateMessagesTokens, reconfigureClient, refreshChatTools, chatTools,
|
|
|
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 { formatArtifactTokenSources } from '../context/artifacts.js';
|
|
25
26
|
import { manualCompact, contextState, newSessionId, saveSession, loadSession, listSessions, appendCurrentSessionRuntimeEvent, hashTraceValue, } from '../session/index.js';
|
|
26
27
|
import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, getCurrentTurnId, } from '../rollback/index.js';
|
|
27
28
|
import { listSkills, effectiveSystemPrompt, } from '../skills/index.js';
|
|
@@ -223,11 +224,15 @@ function renderContextBar(history) {
|
|
|
223
224
|
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.accent;
|
|
224
225
|
const lifecycle = contextState.lifecycleStats;
|
|
225
226
|
const archived = computePruneStats(history);
|
|
227
|
+
const artifactStats = contextState.artifactStats;
|
|
228
|
+
const artifactLine = artifactStats
|
|
229
|
+
? `\n artifacts · fresh ${artifactStats.fresh} · stale ${artifactStats.stale} · stubbed ${archived.stubbed} · tokens ${formatArtifactTokenSources(artifactStats)}`
|
|
230
|
+
: '\n artifacts · no file-backed facts recorded';
|
|
226
231
|
const lifecycleLine = lifecycle
|
|
227
232
|
? `\n lifecycle · live ${lifecycle.live} · referenced ${lifecycle.referenced} · digested ${lifecycle.digested} · stubbed ${lifecycle.stubbed}`
|
|
228
233
|
: '\n lifecycle · no active snapshot (run a tool-enabled turn first)';
|
|
229
234
|
const archiveLine = `\n archived tool results · ${archived.stubbed}`;
|
|
230
|
-
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${Math.round(pct * 100)}% ${k(est)}/${k(win)} tokens · ${t('status.messages', { count: history.length })} (${src})${ui.reset}${lifecycleLine}${archiveLine}`;
|
|
235
|
+
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${Math.round(pct * 100)}% ${k(est)}/${k(win)} tokens · ${t('status.messages', { count: history.length })} (${src})${ui.reset}${artifactLine}${lifecycleLine}${archiveLine}`;
|
|
231
236
|
}
|
|
232
237
|
/** 状态行用量条(精简版,进底栏):[bar] pct% k/k。
|
|
233
238
|
* 只计算对话内容(不含 system prompt),让用户感知"我发了多少、agent 回复了多少"占用 context。 */
|
|
@@ -485,6 +490,10 @@ function echoInput(lines, trailingBlank = true) {
|
|
|
485
490
|
for (const a of pendingAttachments) {
|
|
486
491
|
layout.contentWrite(` ${ui.dim}${renderChip(a)}${ui.reset}\n`);
|
|
487
492
|
}
|
|
493
|
+
// 用户气泡会填满终端整列宽;Windows Terminal 在末列进入 pending-wrap 后,紧随的 LF
|
|
494
|
+
// 偶尔只更新内部滚屏状态,导致上一轮耗时行与新气泡暂时黏连。缓冲中的物理行始终正确,
|
|
495
|
+
// 提交后立即按缓冲重绘,避免必须等滚动或 Agent 结束时的 repaint 才显示正确边界。
|
|
496
|
+
layout.repaintViewport();
|
|
488
497
|
}
|
|
489
498
|
/**
|
|
490
499
|
* 等待 pending 撤回窗口(用户 Enter 后、agent 真发请求前的 500ms 兜底)。
|
|
@@ -637,6 +646,12 @@ export function renderHistory(history) {
|
|
|
637
646
|
}
|
|
638
647
|
const tcs = m.tool_calls;
|
|
639
648
|
if (Array.isArray(tcs) && tcs.length > 0) {
|
|
649
|
+
if (text) {
|
|
650
|
+
// contentWriteMdOnce 会裁掉 markdown 尾部空行,而 history 中的原始 text 是否以 \n
|
|
651
|
+
// 结尾并不能代表当前物理布局。按缓冲中的视觉行归一化,和实时“正文 → 工具”
|
|
652
|
+
// 边界一致地保留一条空行,避免 /resume 回放时工具摘要紧贴正文。
|
|
653
|
+
layout.normalizeMutationBoundary();
|
|
654
|
+
}
|
|
640
655
|
// 累积到 pendingBatch,顺序 = tool_calls 序
|
|
641
656
|
for (const tc of tcs) {
|
|
642
657
|
const name = tc?.function?.name ?? '';
|
|
@@ -717,7 +732,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
717
732
|
// 与开关联动:① base 用 buildBasePrompt() 取代 config.systemPrompt(后者是启动时一次性
|
|
718
733
|
// 求值的常量,运行时 /memory_switch 不会刷新);② plan suffix 走 getPlanModeSuffix() 现拼;
|
|
719
734
|
// ③ buildMemorySection 内已自决 ;④ buildMemoryIndexSection 显式传 isMemoryEnabled() 关闭段。
|
|
720
|
-
const buildSystemMessage = (planMode) => effectiveSystemPrompt(buildBasePrompt() +
|
|
735
|
+
const buildSystemMessage = (planMode) => effectiveSystemPrompt(buildBasePrompt(currentSessionId) +
|
|
721
736
|
(planMode ? getPlanModeSuffix() : '') +
|
|
722
737
|
buildMemorySection() +
|
|
723
738
|
buildMemoryIndexSection(isMemoryEnabled()));
|
|
@@ -891,6 +906,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
891
906
|
rolledBackTurns: Math.max(0, turnCountBeforeRollback - plan.n),
|
|
892
907
|
deletedMessages: rollbackResult.deletedMsgs,
|
|
893
908
|
revertedFiles: rollbackResult.revertedFiles,
|
|
909
|
+
conflictedFiles: rollbackResult.conflictedFiles,
|
|
894
910
|
requestedFileCount: revertPaths.size,
|
|
895
911
|
}, plan.cutoffTurnId);
|
|
896
912
|
try {
|
|
@@ -903,6 +919,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
903
919
|
// 复显剩余对话(无提示行),输入框预填该轮 user 输入 → 下轮 Enter 重新跑
|
|
904
920
|
layout.clearContent();
|
|
905
921
|
renderHistory(history);
|
|
922
|
+
if (rollbackResult.conflictedFiles.length > 0) {
|
|
923
|
+
layout.contentWrite(`${ui.yellow} ⚠ rollback conflict: ${rollbackResult.conflictedFiles.join(', ')} 已在 Agent 提交后变化,未覆盖。${ui.reset}\n`);
|
|
924
|
+
}
|
|
906
925
|
// 末尾补空行:与后续用户消息(❯ bubble)之间分隔。runTurn 在每个 agent 轮结束后
|
|
907
926
|
// contentWrite('\n') 做轮次分隔,/resume 后接 \n\n,/theme·/model 后接 \n;
|
|
908
927
|
// rollbackFlow 原本漏了这一行,renderHistory 末尾的 batch 摘要行 / assistant 文本
|
|
@@ -1021,6 +1040,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1021
1040
|
layout.contentWrite(`${ui.yellow}${t('repl.loadFailed')}${ui.reset}\n`);
|
|
1022
1041
|
return;
|
|
1023
1042
|
}
|
|
1043
|
+
// Bind before rebuilding the prompt, or it can retain the previous session's notes path.
|
|
1044
|
+
currentSessionId = loaded.id;
|
|
1045
|
+
setCurrentSessionId(loaded.id, process.cwd());
|
|
1024
1046
|
if (loaded.history[0]?.role === 'system') {
|
|
1025
1047
|
loaded.history[0] = { role: 'system', content: buildSystemMessage(false) };
|
|
1026
1048
|
}
|
|
@@ -1030,8 +1052,6 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1030
1052
|
? [...loaded.queryHistory]
|
|
1031
1053
|
: queryHistoryFromMessages(loaded.history);
|
|
1032
1054
|
setAgentMode('auto'); // 续接重置为 auto(mode 不落盘;listener 重写 history[0] 回 auto,与 loaded 幂等)
|
|
1033
|
-
currentSessionId = loaded.id;
|
|
1034
|
-
setCurrentSessionId(loaded.id, process.cwd()); // 切换会话:确保该会话的 notes.md 存在
|
|
1035
1055
|
// 读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
|
|
1036
1056
|
if (!loadSnapshots(loaded.id))
|
|
1037
1057
|
rebuildFromHistory(history);
|
|
@@ -1043,6 +1063,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1043
1063
|
// 末尾 \n\n:与后续用户消息(❯ bubble)之间空一行。
|
|
1044
1064
|
layout.contentWrite(`${ui.dim}${t('repl.resumed', { id: loaded.id })}${ui.reset}\n\n`);
|
|
1045
1065
|
}
|
|
1066
|
+
let hasSubmittedInput = false;
|
|
1046
1067
|
while (true) {
|
|
1047
1068
|
// INPUT 态:画底栏输入框 + 状态行,光标入输入框
|
|
1048
1069
|
refreshStatusBase(history);
|
|
@@ -1053,6 +1074,10 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1053
1074
|
layout.contentWrite(` ${ui.gray}↳ ${formatReflectResult(reflectRes)}${ui.reset}\n`);
|
|
1054
1075
|
clearLastReflectResult();
|
|
1055
1076
|
}
|
|
1077
|
+
// 所有斜杠命令和 Agent 轮次共用同一个输出→输入边界,避免某条命令漏写第二个 \n
|
|
1078
|
+
// 后下一条 ❯ 气泡紧贴确认文案;已有多余空行也会收敛为恰好一行。
|
|
1079
|
+
if (hasSubmittedInput)
|
|
1080
|
+
layout.normalizeInputBoundary();
|
|
1056
1081
|
layout.enterInputMode(t('repl.idle'));
|
|
1057
1082
|
let input = null;
|
|
1058
1083
|
try {
|
|
@@ -1080,6 +1105,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1080
1105
|
const line = joined.trim();
|
|
1081
1106
|
if (!line)
|
|
1082
1107
|
continue;
|
|
1108
|
+
hasSubmittedInput = true;
|
|
1083
1109
|
if (line === '/exit' || line === '/quit')
|
|
1084
1110
|
break;
|
|
1085
1111
|
// RUNNING 态:回显输入 → 底栏改 dim 占位、光标回内容续写位
|
|
@@ -2029,7 +2055,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
2029
2055
|
const initPrompt = `请直接初始化或优化当前项目的 Project Skill,并完成写入。
|
|
2030
2056
|
|
|
2031
2057
|
要求:
|
|
2032
|
-
1. 直接由你完成,禁止调用
|
|
2058
|
+
1. 直接由你完成,禁止调用 sub-agent 工具或派生任何子 agent。
|
|
2033
2059
|
2. 优先利用系统提示中已有的 Project Snapshot 和 Project Skill;不要重复扫描其中已有的目录、依赖、命令和模块清单。
|
|
2034
2060
|
3. 最多进行 1 次 codegraph 探索;只有缺少关键依据时,才额外进行少量定点 read_file/grep。禁止全仓 glob 和逐文件扫描。
|
|
2035
2061
|
4. Skill 只记录 Snapshot 无法提供的 WHY/HOW/GOTCHAS/CONVENTIONS:设计取舍、关键调用链、非直觉边界、项目约定和可操作坑点。使用具体路径和例子,删除重复或过时内容。
|